diff --git a/.repos/alchemy-effect/.eslintignore b/.repos/alchemy-effect/.eslintignore new file mode 100644 index 00000000000..c70ce2568c2 --- /dev/null +++ b/.repos/alchemy-effect/.eslintignore @@ -0,0 +1 @@ +alchemy/test/** \ No newline at end of file diff --git a/.repos/alchemy-effect/.github/workflows/deploy.yml b/.repos/alchemy-effect/.github/workflows/deploy.yml new file mode 100644 index 00000000000..0bb41dcc6cc --- /dev/null +++ b/.repos/alchemy-effect/.github/workflows/deploy.yml @@ -0,0 +1,137 @@ +name: Deploy Website + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "website/**" + - "packages/alchemy/**" + - "bun.lock" + - ".github/workflows/deploy.yml" + pull_request: + types: + - opened + - reopened + - synchronize + - closed + paths: + - "website/**" + - "packages/alchemy/**" + - "bun.lock" + - ".github/workflows/deploy.yml" + +concurrency: + group: deploy-website-${{ github.ref }} + cancel-in-progress: false + +env: + STAGE: ${{ github.event_name == 'pull_request' && format('pr-{0}', + github.event.number) || (github.ref == 'refs/heads/main' && 'prod' || + github.ref_name) }} + +jobs: + deploy: + if: ${{ github.event.action != 'closed' }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # Mint a token for the alchemy-version-bot GitHub App so the PR + # preview comment (created via GitHub.Comment in alchemy.run.ts) + # posts under the bot's identity instead of `github-actions[bot]`. + # Same App as release.yml — its installation scopes already cover + # PR comments. + - name: Generate bot token + id: bot-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.ALCHEMY_VERSION_BOT_ID }} + private-key: ${{ secrets.ALCHEMY_VERSION_BOT_PRIVATE_KEY }} + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: Install dependencies + run: bun install + + # Build the website ahead of `alchemy deploy`. The + # Cloudflare.StaticSite resource declares the same `bun run build` + # in its config and runs it as part of the create lifecycle, but + # on update plans alchemy reads `dist/` during plan.diff before + # the embedded Build resource gets a chance to (re)run, causing + # a NotFound on a fresh CI checkout. Building here makes the + # workflow correct under both create and update plans. + - name: Build website + working-directory: website + run: bun run build + + - name: Deploy + working-directory: website + run: bun alchemy deploy --stage ${{ env.STAGE }} --yes + env: + # Alchemy's Cloudflare auth provider accepts either a scoped + # API token (CLOUDFLARE_API_TOKEN) or the legacy global API + # key paired with email (CLOUDFLARE_API_KEY + + # CLOUDFLARE_EMAIL). We use the latter here so CI matches + # the credentials in Doppler / .env, where only the admin + # global key is provisioned. + CLOUDFLARE_API_KEY: ${{ secrets.ADMIN_CLOUDFLARE_API_KEY }} + CLOUDFLARE_EMAIL: ${{ secrets.ADMIN_CLOUDFLARE_EMAIL }} + CLOUDFLARE_ACCOUNT_ID: ${{ env.STAGE == 'prod' && + secrets.PROD_CLOUDFLARE_ACCOUNT_ID || + secrets.TEST_CLOUDFLARE_ACCOUNT_ID }} + PULL_REQUEST: ${{ github.event.number }} + # On `pull_request` events GitHub Actions' built-in + # `GITHUB_SHA` is the synthetic merge commit (PR head merged + # into base) — that SHA never shows up in the PR's git log. + # `GITHUB_SHA` is also a reserved env var, so a step-level + # override is silently ignored. Pass the PR head SHA under + # a non-reserved name and read it from alchemy.run.ts. + BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + GITHUB_TOKEN: ${{ steps.bot-token.outputs.token }} + + cleanup: + runs-on: ubuntu-latest + if: + ${{ github.event_name == 'pull_request' && github.event.action == 'closed' + }} + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Generate bot token + id: bot-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.ALCHEMY_VERSION_BOT_ID }} + private-key: ${{ secrets.ALCHEMY_VERSION_BOT_PRIVATE_KEY }} + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: Install dependencies + run: bun install + + - name: Safety Check + run: |- + if [ "${{ env.STAGE }}" = "prod" ]; then + echo "ERROR: Cannot destroy prod environment in cleanup job" + exit 1 + fi + + - name: Destroy Preview Environment + working-directory: website + run: bun alchemy destroy --stage ${{ env.STAGE }} --yes + env: + CLOUDFLARE_API_KEY: ${{ secrets.ADMIN_CLOUDFLARE_API_KEY }} + CLOUDFLARE_EMAIL: ${{ secrets.ADMIN_CLOUDFLARE_EMAIL }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.TEST_CLOUDFLARE_ACCOUNT_ID }} + PULL_REQUEST: ${{ github.event.number }} + GITHUB_TOKEN: ${{ steps.bot-token.outputs.token }} diff --git a/.repos/alchemy-effect/.github/workflows/pr-package.yaml b/.repos/alchemy-effect/.github/workflows/pr-package.yaml new file mode 100644 index 00000000000..34c9018c0e8 --- /dev/null +++ b/.repos/alchemy-effect/.github/workflows/pr-package.yaml @@ -0,0 +1,180 @@ +name: pr-package + +# Publishes alchemy + better-auth + pr-package tarballs to the pr-package +# service at pkg.ing on every push to main and every PR sync. On PR close, +# deletes every tag we ever assigned for that PR so the underlying tarballs +# become orphaned and the bucket cleans them up. + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened, closed] + +permissions: + contents: read + pull-requests: write + +env: + PR_PACKAGE_HOST: pkg.ing + NODE_VERSION: "24" + +jobs: + # ── Publish on push-to-main and PR sync. ────────────────────────────────── + publish: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && + github.event.action != 'closed') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install + + # Build everything: tsdown bundles alchemy (bin/ + cli lib) and tsc -b + # populates lib/ for the layer packages (better-auth, pr-package). + # Their tarballs ship both src/ and lib/, and consumers in non-bun + # runtimes resolve via the `import` condition → ./lib/index.js. + - run: bun run build:packages + + - name: Compute tags + id: tags + env: + EVENT: ${{ github.event_name }} + # `head_ref` for PRs (source branch), `ref_name` for push (e.g. "main"). + BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || + github.ref_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + # PR builds run on the merge commit by default; tag the actual head sha + # so consumers can pin to a specific PR commit. + SHA: ${{ github.event_name == 'pull_request' && + github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + short="${SHA:0:7}" + long="$SHA" + tags=("$short" "$long" "$BRANCH") + if [ "$EVENT" = "pull_request" ]; then + tags+=("pr-${PR_NUMBER}") + fi + json=$(printf '%s\n' "${tags[@]}" | jq -R . | jq -s -c .) + echo "tags=$json" >> "$GITHUB_OUTPUT" + echo "short=$short" >> "$GITHUB_OUTPUT" + echo "Tags: $json" + + # PR builds expire after 1 week; pushes to main omit the header and + # fall back to the worker's defaultTtl (3 weeks). + - name: Publish alchemy + env: + TOKEN: ${{ secrets.PR_PACKAGE_TOKEN }} + TAGS: ${{ steps.tags.outputs.tags }} + TTL: ${{ github.event_name == 'pull_request' && '1 week' || '' }} + working-directory: packages/alchemy + run: | + set -euo pipefail + rm -f *.tgz + bun pm pack --destination . + tgz=$(ls *.tgz) + echo "Publishing $tgz with tags $TAGS ttl=${TTL:-default}" + curl -fsSL --show-error -X PUT \ + "https://${PR_PACKAGE_HOST}/projects/alchemy/packages" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "X-Tags: ${TAGS}" \ + ${TTL:+-H "X-TTL: ${TTL}"} \ + -H "Content-Type: application/gzip" \ + --data-binary "@${tgz}" + + - name: Publish better-auth + env: + TOKEN: ${{ secrets.PR_PACKAGE_TOKEN }} + TAGS: ${{ steps.tags.outputs.tags }} + TTL: ${{ github.event_name == 'pull_request' && '1 week' || '' }} + working-directory: packages/better-auth + run: | + set -euo pipefail + rm -f *.tgz + bun pm pack --destination . + tgz=$(ls *.tgz) + echo "Publishing $tgz with tags $TAGS ttl=${TTL:-default}" + curl -fsSL --show-error -X PUT \ + "https://${PR_PACKAGE_HOST}/projects/@alchemy.run/better-auth/packages" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "X-Tags: ${TAGS}" \ + ${TTL:+-H "X-TTL: ${TTL}"} \ + -H "Content-Type: application/gzip" \ + --data-binary "@${tgz}" + + - name: Publish pr-package + env: + TOKEN: ${{ secrets.PR_PACKAGE_TOKEN }} + TAGS: ${{ steps.tags.outputs.tags }} + TTL: ${{ github.event_name == 'pull_request' && '1 week' || '' }} + working-directory: packages/pr-package + run: | + set -euo pipefail + rm -f *.tgz + bun pm pack --destination . + tgz=$(ls *.tgz) + echo "Publishing $tgz with tags $TAGS ttl=${TTL:-default}" + curl -fsSL --show-error -X PUT \ + "https://${PR_PACKAGE_HOST}/projects/@alchemy.run/pr-package/packages" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "X-Tags: ${TAGS}" \ + ${TTL:+-H "X-TTL: ${TTL}"} \ + -H "Content-Type: application/gzip" \ + --data-binary "@${tgz}" + + # PR-only: leave a sticky comment with the install URLs pinned to this + # commit. Uses pkg.ing (the canonical short host), which serves both + # the pretty alias paths and the underlying /projects/:project/tags/:tag + # API directly. + - name: Generate bot token + if: github.event_name == 'pull_request' + id: bot-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.ALCHEMY_VERSION_BOT_ID }} + private-key: ${{ secrets.ALCHEMY_VERSION_BOT_PRIVATE_KEY }} + + - name: Comment on PR + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SHORT_SHA: ${{ steps.tags.outputs.short }} + run: | + set -euo pipefail + MARKER="" + # Use `bun add @` instead of `bun add `. Without an + # explicit name, bun's resolver hits a DependencyLoop bug when the + # consumer already has the same package name resolved from npm + # (oven-sh/bun#5789, oven-sh/bun#17946). The aliased form bypasses + # the buggy reconciliation path and works on fresh and existing + # projects alike. + BODY=$(printf '%s\n\nInstall the packages built from this commit:\n\n**alchemy**\n```sh\nbun add alchemy@https://pkg.ing/alchemy/%s\n```\n\n**@alchemy.run/better-auth**\n```sh\nbun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/%s\n```\n\n**@alchemy.run/pr-package**\n```sh\nbun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/%s\n```\n' "$MARKER" "$SHORT_SHA" "$SHORT_SHA" "$SHORT_SHA") + + existing=$(gh api --paginate \ + "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq ".[] | select(.user.login == \"alchemy-version-bot[bot]\") | select(.body | startswith(\"${MARKER}\")) | .id" \ + | head -1) + + if [ -n "$existing" ]; then + gh api -X PATCH "repos/${REPO}/issues/comments/${existing}" -f body="$BODY" + else + gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/comments" -f body="$BODY" + fi diff --git a/.repos/alchemy-effect/.github/workflows/release.yml b/.repos/alchemy-effect/.github/workflows/release.yml new file mode 100644 index 00000000000..e41f2700040 --- /dev/null +++ b/.repos/alchemy-effect/.github/workflows/release.yml @@ -0,0 +1,432 @@ +name: Release NPM Package + +on: + workflow_dispatch: + inputs: + channel: + description: "Release channel" + required: true + type: choice + default: beta + options: + - release + - beta + - alpha + - tag + spec: + description: >- + Channel-specific value. tag: REQUIRED, the explicit full version (e.g. + 2.0.0-experimental.1). release: REQUIRED, patch|minor|major or an + explicit x.y.z. beta|alpha: optional, an integer N to force + 2.0.0-.N; leave blank to auto-increment. + required: false + type: string + +permissions: + contents: write + id-token: write + +env: + NODE_VERSION: "24" + # Files produced by the bump job and consumed by every publish job + the + # final commit job. Listed once here so additions stay in sync. + BUMP_ARTIFACT_PATHS: | + packages/alchemy/lib + packages/alchemy/bin + packages/alchemy/package.json + packages/better-auth/package.json + packages/better-auth/lib + packages/pr-package/package.json + packages/pr-package/lib + bun.lock +jobs: + # ── Step 1: Compute the next version and stage the bump in-memory. ── + # No git commit: we want a failed publish to leave no orphan commit + # behind. The staged files ride to the publish jobs (and the final commit + # job) as an artifact. + bump: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.bump.outputs.version }} + channel: ${{ inputs.channel }} + sha: ${{ steps.sha.outputs.sha }} + steps: + - name: Validate inputs + env: + CHANNEL: ${{ inputs.channel }} + SPEC: ${{ inputs.spec }} + run: | + case "$CHANNEL" in + tag) + if [ -z "$SPEC" ]; then + echo "::error::'tag' channel requires an explicit version in 'spec' (e.g. 2.0.0-experimental.1)" + exit 1 + fi + ;; + release) + if [ -z "$SPEC" ]; then + echo "::error::'release' channel requires a 'spec' (patch|minor|major|x.y.z)" + exit 1 + fi + ;; + esac + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + + - id: sha + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + registry-url: https://registry.npmjs.org/ + node-version: ${{ env.NODE_VERSION }} + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install + + - id: bump + env: + CHANNEL: ${{ inputs.channel }} + SPEC: ${{ inputs.spec }} + run: | + if [ -n "$SPEC" ]; then + VERSION=$(bun ./scripts/release/bump.ts "$CHANNEL" "$SPEC") + else + VERSION=$(bun ./scripts/release/bump.ts "$CHANNEL") + fi + echo "Resolved version: $VERSION (channel: $CHANNEL)" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # Build everything once, up front, against the bumped versions. Outputs + # ride to every publish job as the `build-output` artifact so we don't + # rebuild three times in parallel. + - run: bun run build:packages + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: bump-files + path: ${{ env.BUMP_ARTIFACT_PATHS }} + if-no-files-found: error + retention-days: 7 + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-output + path: | + packages/*/lib + packages/*/bin + if-no-files-found: error + retention-days: 7 + include-hidden-files: true + + # ── Step 2: Commit (bump + changelog) and tag BEFORE publishing. + # Committing first means the publishable source of truth is the tagged + # commit; publish jobs check it out by SHA. Durability: if a previous + # attempt already committed and tagged but failed during npm publish, the + # bump job's HEAD-tag detection reuses the existing version, and this job + # detects HEAD is already at the tag and skips the commit/push. + # Skipped for `tag` channel — those releases are intentionally uncommitted. + commit-and-tag: + needs: bump + if: inputs.channel != 'tag' + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.commit.outputs.sha }} + steps: + - name: Generate bot token + id: bot-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.ALCHEMY_VERSION_BOT_ID }} + private-key: ${{ secrets.ALCHEMY_VERSION_BOT_PRIVATE_KEY }} + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + token: ${{ steps.bot-token.outputs.token }} + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: bump-files + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install + + - name: Configure git + run: | + git config user.email "alchemy-version-bot[bot]@users.noreply.github.com" + git config user.name "alchemy-version-bot[bot]" + + - id: commit + name: Commit bump + changelog, tag, push + env: + VERSION: ${{ needs.bump.outputs.version }} + run: | + set -euo pipefail + TAG="v${VERSION}" + + # Durability: if HEAD already points at the release tag (resumed + # run after a previous attempt committed+tagged but publish + # failed), there is nothing to do. Use HEAD as the publish SHA. + HEAD_TAG=$(git describe --exact-match --tags HEAD 2>/dev/null || true) + if [ "$HEAD_TAG" = "$TAG" ]; then + echo "HEAD is already at ${TAG}; skipping commit/tag/push" + SHA=$(git rev-parse HEAD) + echo "sha=${SHA}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Generate release notes; release-notes.ts is idempotent and + # bails out if the tag heading is already present in CHANGELOG.md. + bun scripts/release/release-notes.ts "${TAG}" + + # Stage bump files (overwritten by the downloaded artifact) and + # the new changelog entry as a single commit. + git add packages/alchemy/package.json \ + packages/better-auth/package.json \ + packages/pr-package/package.json \ + bun.lock \ + CHANGELOG.md + + if git diff --cached --quiet; then + echo "No changes to commit (already on a release commit?)" + else + git commit -m "chore(release): ${VERSION}" + fi + + if ! git rev-parse --verify "refs/tags/${TAG}" >/dev/null 2>&1; then + git tag -a "${TAG}" -m "Release ${TAG}" + fi + + git push origin HEAD + if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "Tag ${TAG} already on remote, skipping tag push" + else + git push origin "refs/tags/${TAG}" + fi + + SHA=$(git rev-parse HEAD) + echo "sha=${SHA}" >> "$GITHUB_OUTPUT" + + # ── Step 3a: Build and publish `alchemy`. ── + # For non-tag channels, checkout the just-committed tagged SHA; for the + # `tag` channel (no commit), checkout the bump's pre-bump SHA and rely + # on the bump-files artifact instead. + publish-alchemy: + needs: [bump, commit-and-tag] + if: + always() && needs.bump.result == 'success' && (needs.commit-and-tag.result + == 'success' || needs.commit-and-tag.result == 'skipped') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.commit-and-tag.outputs.sha || needs.bump.outputs.sha }} + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + registry-url: https://registry.npmjs.org/ + node-version: ${{ env.NODE_VERSION }} + + - name: Upgrade npm for OIDC trusted publishing + run: | + npm install -g npm@latest && npm --version + sed -i '/always-auth/d' ~/.npmrc 2>/dev/null || true + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: bump-files + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-output + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install + + - run: bun scripts/release/publish-package.ts packages/alchemy ${{ + needs.bump.outputs.channel }} + + # ── Step 3b: Build and publish `@alchemy.run/better-auth`. ── + publish-better-auth: + needs: [bump, commit-and-tag, publish-alchemy] + if: + always() && needs.bump.result == 'success' && (needs.commit-and-tag.result + == 'success' || needs.commit-and-tag.result == 'skipped') && + needs.publish-alchemy.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.commit-and-tag.outputs.sha || needs.bump.outputs.sha }} + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + registry-url: https://registry.npmjs.org/ + node-version: ${{ env.NODE_VERSION }} + + - name: Upgrade npm for OIDC trusted publishing + run: | + npm install -g npm@latest && npm --version + sed -i '/always-auth/d' ~/.npmrc 2>/dev/null || true + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: bump-files + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-output + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install + + - run: bun scripts/release/publish-package.ts packages/better-auth ${{ + needs.bump.outputs.channel }} + + # ── Step 3c: Build and publish `@alchemy.run/pr-package`. ── + publish-pr-package: + needs: [bump, commit-and-tag, publish-alchemy] + if: + always() && needs.bump.result == 'success' && (needs.commit-and-tag.result + == 'success' || needs.commit-and-tag.result == 'skipped') && + needs.publish-alchemy.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.commit-and-tag.outputs.sha || needs.bump.outputs.sha }} + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + registry-url: https://registry.npmjs.org/ + node-version: ${{ env.NODE_VERSION }} + + - name: Upgrade npm for OIDC trusted publishing + run: | + npm install -g npm@latest && npm --version + sed -i '/always-auth/d' ~/.npmrc 2>/dev/null || true + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: bump-files + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-output + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install + + - run: bun scripts/release/publish-package.ts packages/pr-package ${{ + needs.bump.outputs.channel }} + + # ── Step 4: Create GitHub Release and notify Discord. ── + # Runs only after every publish job succeeded. The bump+changelog commit + # and tag were already pushed by `commit-and-tag` before publishing. + finalize: + needs: + [ + bump, + commit-and-tag, + publish-alchemy, + publish-better-auth, + publish-pr-package, + ] + if: inputs.channel != 'tag' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.commit-and-tag.outputs.sha }} + fetch-depth: 0 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install + + - name: Create GitHub Release + env: + VERSION: ${{ needs.bump.outputs.version }} + CHANNEL: ${{ needs.bump.outputs.channel }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bun scripts/release/github-release.ts "v${VERSION}" "${CHANNEL}" + + - name: Notify Discord + if: success() && env.DISCORD_WEBHOOK != '' + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} + VERSION: ${{ needs.bump.outputs.version }} + CHANNEL: ${{ needs.bump.outputs.channel }} + run: bun scripts/release/discord-notify.ts "v${VERSION}" "${CHANNEL}" diff --git a/.repos/alchemy-effect/.github/workflows/test.yml b/.repos/alchemy-effect/.github/workflows/test.yml new file mode 100644 index 00000000000..56a8d010cab --- /dev/null +++ b/.repos/alchemy-effect/.github/workflows/test.yml @@ -0,0 +1,93 @@ +name: test + +# Smoke-tests `alchemy destroy → deploy → destroy` against each example. +# `bun test:canary` drives the whole suite — it iterates both bun and pnpm +# runtimes itself and publishes canary tarballs to pkg.ing before deploying. + +on: + # TODO(sam): re-enable these when they are running reliably and not polluting our account + # push: + # branches: [main] + # pull_request: + # types: [opened, reopened, synchronize] + workflow_dispatch: + +permissions: + contents: read + # Required for aws-actions/configure-aws-credentials to mint a session + # via OIDC instead of long-lived AWS_ACCESS_KEY_ID secrets. + id-token: write + +env: + PKGING_HOST: pkg.ing + NODE_VERSION: "24" + # PR runs use `pr-` so each PR has its own isolated cloud namespace; + # main pushes use `main`; manual `workflow_dispatch` runs use the branch + # name. Forwarded to the smoke test as $SMOKE_STAGE. + SMOKE_STAGE: ${{ github.event_name == 'pull_request' && format('pr-{0}', + github.event.number) || (github.ref == 'refs/heads/main' && 'main' || + github.ref_name) }} + +concurrency: + # Cancel an in-progress run for the same PR (or branch) when a new push + # arrives — the smoke suite mutates real cloud state, so two parallel + # runs against the same SMOKE_STAGE would race. + group: test-${{ github.ref }} + cancel-in-progress: true + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + with: + version: latest + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: bun-${{ runner.os }}- + + # Bootstrap install so `bun test:canary` itself can resolve. The + # test then runs its own `bun install` + `pnpm install` in canary + # `beforeAll` after rewriting the catalog to pkg.ing tarballs, so + # there's nothing pnpm-specific to wire up here. + - run: bun install + + # OIDC → short-lived AWS session. Exports AWS_ACCESS_KEY_ID, + # AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION; the + # `output-credentials: true` flag also surfaces aws-account-id as a + # step output (which we forward as `AWS_ACCOUNT_ID` below — the + # action does NOT set it as an env var on its own). + - id: aws-creds + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ vars.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + output-credentials: true + + - name: Smoke + env: + # AWS — `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, + # `AWS_SESSION_TOKEN`, `AWS_REGION` are already exported by + # configure-aws-credentials; we only need to forward the account + # id, which Alchemy's `loadFromEnv` requires when CI=true. + AWS_ACCOUNT_ID: ${{ steps.aws-creds.outputs.aws-account-id }} + CLOUDFLARE_API_TOKEN: ${{ secrets.TEST_CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.TEST_CLOUDFLARE_ACCOUNT_ID }} + NEON_API_KEY: ${{ secrets.NEON_API_KEY }} + PR_PACKAGE_TOKEN: ${{ secrets.PR_PACKAGE_TOKEN }} + PKGING_HOST: ${{ env.PKGING_HOST }} + SMOKE_STAGE: ${{ env.SMOKE_STAGE }} + run: bun test:canary diff --git a/.repos/alchemy-effect/.gitignore b/.repos/alchemy-effect/.gitignore new file mode 100644 index 00000000000..841879bb912 --- /dev/null +++ b/.repos/alchemy-effect/.gitignore @@ -0,0 +1,48 @@ +node_modules/ +dist/ +/lib/ +/alchemy/lib/ +.out/ +.test/ +.DS_Store +.env +*.tsbuildinfo +.claude/ +.alchemy/ +# !.alchemy/github:alchemy/ +# examples/*/.alchemy + +.repomix-output.txt +wrangler.jsonc + +# Husky - ignore generated files but keep hooks +.husky/_ +!.husky/pre-commit + +.smoke +.smoke.logs +smoke +# Generated by test/smoke.test.ts to make pnpm 11's deps-status check +# resolve the bun catalog. Cleaned up at end of suite. +pnpm-workspace.yaml +.svelte-kit +.astro +.wrangler + +*.tgz +*.js +*.d.ts +*.map + +.attest +.cache +.external +.bundle-benchmark +packages/alchemy/docs +.sonda +examples/cloudflare-worker/src/scratch.ts + +!/packages/alchemy/bin/cli.js +.vendor/* +.tmp +pnpm-lock.yaml \ No newline at end of file diff --git a/.repos/alchemy-effect/.gitmodules b/.repos/alchemy-effect/.gitmodules new file mode 100644 index 00000000000..2f258481075 --- /dev/null +++ b/.repos/alchemy-effect/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/alchemy"] + path = .vendor/alchemy + url = git@github.com:alchemy-run/alchemy.git diff --git a/.repos/alchemy-effect/.husky/pre-commit b/.repos/alchemy-effect/.husky/pre-commit new file mode 100644 index 00000000000..172915addb7 --- /dev/null +++ b/.repos/alchemy-effect/.husky/pre-commit @@ -0,0 +1,2 @@ +bun format +git update-index --again diff --git a/.repos/alchemy-effect/.ignore b/.repos/alchemy-effect/.ignore new file mode 100644 index 00000000000..6f0ec623987 --- /dev/null +++ b/.repos/alchemy-effect/.ignore @@ -0,0 +1 @@ +# see: https://opencode.ai/docs/tools/#ignore-patterns \ No newline at end of file diff --git a/.repos/alchemy-effect/.oxfmtrc.json b/.repos/alchemy-effect/.oxfmtrc.json new file mode 100644 index 00000000000..3091cbb48a9 --- /dev/null +++ b/.repos/alchemy-effect/.oxfmtrc.json @@ -0,0 +1,22 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "useTabs": false, + "printWidth": 80, + "endOfLine": "lf", + "ternaries": true, + "experimental_sort_imports": { + "order": "asc" + }, + "ignorePatterns": [ + "dist/**", + "*.min.js", + "**/lib/**", + "**/mdx/**", + "**/*.mdx", + "**/*.md", + "**/.vendor/**", + "**/package.json" + ] +} diff --git a/.repos/alchemy-effect/.oxlintrc.json b/.repos/alchemy-effect/.oxlintrc.json new file mode 100644 index 00000000000..5ec7dbbbb64 --- /dev/null +++ b/.repos/alchemy-effect/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "rules": { + "no-misused-new": "off", + "require-yield": "off", + "no-non-null-asserted-optional-chain": "off" + } +} diff --git a/.repos/alchemy-effect/.vendor/alchemy b/.repos/alchemy-effect/.vendor/alchemy new file mode 160000 index 00000000000..c9f5e549cf0 --- /dev/null +++ b/.repos/alchemy-effect/.vendor/alchemy @@ -0,0 +1 @@ +Subproject commit c9f5e549cf023632c3df948c207a58336192b3c7 diff --git a/.repos/alchemy-effect/.vscode/settings.json b/.repos/alchemy-effect/.vscode/settings.json new file mode 100644 index 00000000000..5507ee50273 --- /dev/null +++ b/.repos/alchemy-effect/.vscode/settings.json @@ -0,0 +1,65 @@ +{ + "diffEditor.renderSideBySide": false, + "js/ts.experimental.useTsgo": true, + "typescript.experimental.useTsgo": true, + "typescript.tsdk": "node_modules/@typescript/native-preview/lib", + "typescript.native-preview.tsdk": "node_modules/@typescript/native-preview/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.importModuleSpecifierEnding": "js", + "oxc.enable": true, + "oxc.lint.run": "onSave", + "json.schemaDownload.enable": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.codeLens": false, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "editor.inlayHints.enabled": "offUnlessPressed", + "editor.tabSize": 2, + "editor.indentSize": 2, + "editor.insertSpaces": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[markdown]": { + "editor.formatOnSave": false, + "editor.defaultFormatter": null + }, + "[mdx]": { + "editor.defaultFormatter": null, + "editor.formatOnSave": false + }, + "files.exclude": { + "**/__pycache__": true, + "**/*.pyc": true, + "packages/alchemy/bin/**/*.js*": true, + "packages/alchemy/bin/**/*.d*": true + }, + "[typescript]": { + "editor.indentSize": 2, + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[javascript]": { + "editor.indentSize": 2, + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescriptreact]": { + "editor.indentSize": 2, + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[javascriptreact]": { + "editor.indentSize": 2, + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "oxc.enable.oxfmt": true +} diff --git a/.repos/alchemy-effect/AGENTS.md b/.repos/alchemy-effect/AGENTS.md new file mode 100644 index 00000000000..2d3e71d8b07 --- /dev/null +++ b/.repos/alchemy-effect/AGENTS.md @@ -0,0 +1,854 @@ +# alchemy + +Alchemy Effect is an Infrastructure-as-Effects (IaE) framework that extends Infrastructure-as-Code (IaC) by combining business logic and infrastructure config into a single, type-safe program expressed as Effects. + +It includes a core IaC engine built with Effect. Effect provides the foundation for type-safe, composable, and testable infrastructure programs. It brings errors into the type-system and provides declarative/composable retry logic that ensure proper and reliable handling of failures. + +# Concepts + +- **Cloud Provider** - a cloud provider that offers a set of Services, e.g. AWS, Azure, GCP, Cloudflare, Stripe, Planetscale, Neon, etc. +- **Service** - a collection of Resources, Functions, and Bindings offered by a Cloud Provider. +- **Resource** - a named entity that is configuted with "Input Properties" and produces "Output Attributes". May or may not have Binding Contract. +- **Input Properties** - the properties passed as input to configure a Resource. Otherwise known as the "desired state" of the Resource. +- **Output Attributes** - the attributes produced by a Resource. Otherwise known as the "current state" of the Resource. +- **Stable Properties** - properties that are not affected by an Update, e.g. the ID or ARN of a Resource. +- **Function** (aka. **Runtime**) - a special kind of Resource that includes a runtime implementation expressed as a Function producing an `Effect`. The `Req` type captures runtime dependencies, from which Infrastructure Dependencies are inferred. +- **Resource Provider** (see [Provider](./packages/alchemy/src/Provider.ts)) + +A Resource Provider implements the following Lifecycle Operations: + +- **Diff** - compares new props with old props and determines if the Resource needs to be updated or replaced. For updates, it can also specify a list of Stable Properties that will not be changed by the update. +- **Read** - reads the current state of a Resource and returns the current Output Attributes. May return `Unowned(attrs)` to signal an existing-but-foreign resource that the engine should refuse to take over unless `--adopt` is set. +- **Pre-Create** - an optional operation that creates a stub of a Resource before reconcile runs. Used to resolve circular dependencies — e.g. Function A and B depend on each other, so we create a stub of Function A first and then `reconcile` later wires up the real dependency. +- **Reconcile** - converges a Resource's actual cloud state to the desired state described by the new Input Properties. Called for both first-time provisioning and subsequent updates. The provider receives `output` (current Attributes) and `olds` (previous Props) which may both be `undefined` on a greenfield create, both defined on an update, or `output !== undefined && olds === undefined` on an adoption. See the **Reconciler doctrine** section below for the required shape. +- **Delete** - deletes an existing Resource. It must be designed as idempotent because it is always possible for state persistence to fail after the delete operation is called. If the resource doesn't exist during deletion, it should not be considered an error. +- **Capability** - a runtime requirement of a Function (e.g. require `SQS.SendMessage` on a `SQS.Queue`). Each Capability is split into two parts: a `Binding.Service` (runtime SDK wrapper) and a `Binding.Policy` (deploy-time IAM/binding attachment). +- **Binding.Service** - an Effect Service that wraps an SDK client and exposes a `.bind(resource)` method returning a typed callable for runtime use. Provided as a Layer on the **Function** Effect so it gets bundled into the Lambda/Worker. See [Binding](./packages/alchemy/src/Binding.ts). +- **Binding.Policy** - an Effect Service that runs only at deploy time to attach IAM policies (AWS) or bindings (Cloudflare) to a Function's role/config. At runtime, `Binding.Policy` uses `Effect.serviceOption` so it gracefully becomes a no-op when the layer is not provided. Policy layers are provided on the **Stack** via `AWS.providers()()`, not on the Function. +- **Binding** - data attached to a Resource via `resource.bind(data)`. A Binding is a `{ context: PolicyContext, data: BindingData }` tuple that is collected on the Stack during plan/deploy. Bindings enable circular references between Resources — the `Binding.Policy` calls `ctx.bind({ policyStatements: [...] })` on the target Function, which records the binding data on the Stack. The Resource Provider then receives the resolved binding data in its `reconcile` lifecycle operation via the `bindings` parameter. +- **Binding Contract** - the shape of data a Resource accepts from Bindings. For example, a Lambda Function accepts `{ env?: Record, policyStatements?: PolicyStatement[] }` because it needs environment variables and IAM policies. A Cloudflare Worker accepts `{ bindings: Worker.Binding[] }` for its native binding system. The Binding Contract is declared as the fourth type parameter on the `Resource` interface. See [Lambda Function](./packages/alchemy/src/AWS/Lambda/Function.ts) and [Cloudflare Worker](./packages/alchemy/src/Cloudflare/Workers/Worker.ts). +- **Dependency** - Resources depend on other Resources through two mechanisms: + - Output Properties of one Resource passed as Input Properties to another Resource (non-circular, directed acyclic graph) + - Bindings that attach data (IAM policies, env vars, Cloudflare bindings) from one Resource to another, enabling circular references between Resources. +- **Output** - a reference to (or derived from) a Resource's "Output Attributes". E.g. Bucket.bucketArn +- **Stack** - a collection of Resources, Functions, and Bindings that are deployed together. +- **Stack Name** - the name of a Stack, e.g. `my-stack` +- **Stage** - the stage of a Stack, e.g. `dev`, `prod`, `dev-sam` +- **Stack Instance** - a deployed instance of a Stack+Stage +- **Resource Type** - the type of a Resource, e.g. `Bucket`, `Instance` +- **Physical Name** - a unique name for a Resource, e.g. `my-bucket-1234567890`. It is usually best to generate them using the built-in createPhysicalName utility function which generates +- **Logical ID** - the logical ID identifying a resource within a Stack, e.g. `my-bucket`. It is stable across creates, updates, deletes and replaces. +- **Instance ID** - a unique identifier for an instance of a Resource. It is stable across creates, updates and deletes. It changes when a resource is replaced. It is truncated and used as the suffix of the Physical Name. +- **Event Source** - a special kind of Binding between a Function and a Resource that produces events that invoke the Function, e.g. `SQS.QueueEventSource`. Event Sources are implemented as Binding.Service + Binding.Policy pairs, where the attach logic creates/updates the event source mapping via the cloud provider API. +- **Replacement** - the process of replacing a Resource with a new one. A new one is created, downstream dependencies are updated with the new reference, and then the old one is deleted. Or, the old one is deleted first and then the new one is created. +- **Dependency Violation** - an error that some APIs call when an operation cannot be performed because a dependency is not met. E.g. you cannot delete an EIP until the NAT Gateway it is attached to is deleted. Lifecycle operations typically retry Dependency Violations. +- **Eventual Consistency** - create/update/delete operations can be eventually consistent leading to a variety of failure modes. For example, a Resource may be created but not yet available for use, or a Resource may be deleted but still appear in the console. Errors caused by eventual consistency should be retried, and lifecycle operations/tests should be carefully designed to wait for consistency before proceeding. +- **Retryable Error** - an error that can be retried. E.g. a Dependency Violation, Eventual Consistency Error, Transient Failure, etc. +- **Non-Retryable Error** - an error that cannot be retried. E.g. a Validation Error, Authorization Error, etc. +- **Retry Policy** - a policy for retrying errors. E.g. a fixed delay, exponential backoff, max retries, while some condition is true, or until some condition is true/false, etc. + +# File System Conventions + +Each Service's Resources follow the same pattern. Resource contract and provider are co-located in the same file. Capabilities (Binding.Service + Binding.Policy) are in separate files named after the capability. + +```sh +# source files +packages/alchemy/src/{Cloud}/{Service}/index.ts # re-exports all resources and capabilities +packages/alchemy/src/{Cloud}/{Service}/{Resource}.ts # resource contract + resource provider +packages/alchemy/src/{Cloud}/{Service}/{Capability}.ts # Binding.Service + Binding.Policy for a capability +# test files +packages/alchemy/test/{Cloud}/{Service}/{Resource}.test.ts +# docs (auto-generated from source-code JSDoc - DO NOT manually edit) +website/src/content/docs/providers/{Cloud}/{Resource}.md # API reference, generated by `bun generate:api-reference` +``` + +Examples of actual paths: + +```sh +packages/alchemy/src/AWS/S3/Bucket.ts # S3 Bucket resource + provider +packages/alchemy/src/AWS/S3/GetObject.ts # S3 GetObject Binding.Service + Binding.Policy +packages/alchemy/src/AWS/S3/PutObject.ts # S3 PutObject Binding.Service + Binding.Policy +packages/alchemy/src/AWS/SQS/Queue.ts # SQS Queue resource + provider +packages/alchemy/src/AWS/SQS/SendMessage.ts # SQS SendMessage capability +packages/alchemy/src/AWS/Kinesis/Stream.ts # Kinesis Stream resource + provider +packages/alchemy/src/AWS/Kinesis/PutRecord.ts # Kinesis PutRecord capability +packages/alchemy/src/AWS/Lambda/Function.ts # Lambda Function resource + provider +packages/alchemy/src/AWS/DynamoDB/Table.ts # DynamoDB Table resource + provider +packages/alchemy/src/AWS/DynamoDB/GetItem.ts # DynamoDB GetItem capability +packages/alchemy/src/AWS/EC2/Vpc.ts # VPC resource + provider +packages/alchemy/src/AWS/EC2/Subnet.ts # Subnet resource + provider +``` + +# Documentation Generation + +**Source of truth:** The source code is the single source of truth for all API documentation. JSDoc comments in `packages/alchemy/src/**/*.ts` are extracted and used to generate the public API reference markdown. + +:::warning +**Never edit the generated markdown files** under `website/src/content/docs/providers/{Cloud}/`. They are overwritten on every regeneration. + +To "update the docs", edit the JSDoc on the source `.ts` file (resource-level JSDoc on the exported `const`, plus field-level JSDoc on each prop/attribute) and re-run the generator. There is no separate doc file to update. +::: + +**How to generate docs:** + +```sh +bun generate:api-reference # -> website/src/content/docs/providers/{Cloud}/{Resource}.md +``` + +This is the only doc generator that produces user-facing output. ([scripts/generate-api-reference.ts](./scripts/generate-api-reference.ts)) does the following: + +1. Discovers resource files in `packages/alchemy/src/{Cloud}/{Service}/` +2. Parses TypeScript with `ts-morph` +3. Extracts the resource-level summary plus `@section` / `@example` blocks from JSDoc +4. Writes one markdown file per resource at `website/src/content/docs/providers/{Cloud}/{Resource}.md` + +After editing JSDoc on a resource, run `bun generate:api-reference` to refresh the website docs. + +**Writing good documentation:** When adding or updating a resource, ensure all Props and Attrs have JSDoc comments: + +```typescript +export interface BucketProps { + /** + * Name of the bucket. If omitted, a unique name will be generated. + * Must be lowercase and between 3-63 characters. + */ + bucketName?: string; + + /** + * Whether to delete all objects when the bucket is destroyed. + * @default false + */ + forceDestroy?: boolean; +} +``` + +The `@default` tag is used to document default values and will appear in the generated documentation. + +### Examples and Sections (IMPORTANT) + +**Examples are critical for documentation.** Every resource should have examples demonstrating common use cases. Use `@section` and `@example` JSDoc tags on the main Resource export to organize examples into a navigable table of contents. + +**Format:** + +- `@section
` - Creates a heading in the Examples section and adds an entry to the Quick Reference table of contents +- `@example ` - Creates a subheading for a specific code example (must follow a `@section`) +- Code blocks inside examples use standard markdown fenced code blocks (` `) + +**Example:** + +````typescript +/** + * An S3 bucket for storing objects. + * + * @section Creating a Bucket + * @example Basic Bucket + * ```typescript + * const bucket = yield* Bucket("my-bucket", {}); + * ``` + * + * @example Bucket with Force Destroy + * ```typescript + * const bucket = yield* Bucket("my-bucket", { + * forceDestroy: true, + * }); + * ``` + * + * @section Reading Objects + * @example Get Object from Bucket + * ```typescript + * const response = yield* getObject(bucket, { key: "my-key" }); + * const body = yield* Effect.tryPromise(() => response.Body?.transformToString()); + * ``` + * + * @section Writing Objects + * @example Put Object to Bucket + * ```typescript + * yield* putObject(bucket, { + * key: "hello.txt", + * body: "Hello, World!", + * contentType: "text/plain", + * }); + * ``` + */ +export const Bucket = Resource<...>("AWS.S3.Bucket"); +```` + +This generates: + +1. A "Quick Reference" section with links to each `@section` +2. An "Examples" section with organized code examples under each section heading + +**Best practices for examples:** + +- Start with the simplest use case and progress to more complex ones +- Include examples for all major capabilities (GetObject, PutObject, etc.) +- Show real-world patterns like error handling, combining with other resources +- Use descriptive titles that explain what the example demonstrates + +# Workflow + +Development of Alchemy-Effect Resources is heavily pattern based. Each Service has many Resources that each have 0 oor more Capabilities and Event Sources. When working on a new Service, the following steps should be followed. + +1. Research the AWS Service and identify its Resources, Identifier Types, Structs, Capabilities, and Event Sources. Refer to the corresponding Terraform Provider, Pulumi Provider, and CloudFormation docs for that service (use the provided tools specifically for searching these docs for services and resources). + +Example (abbreviated): + +Service: S3 + +Resources: + +- Bucket +- BucketPolicy +- etc. + +Bucket Capabilities: + +- GetObject +- PutObject +- DeleteObject + +Identifier Types: + +- Bucket Name +- Bucket ARN + +Structs: + +- CorsRule +- LifecycleConfiguration + +2. Document each of the Resource interfaces + +Include the following information: + +- ResourceName, e.g. Bucket, Instance, Queue +- Input Properties (for each property: Name, Type, Description, Default Value, Required, Constraints, Replaces: true/false) +- Output Attributes (for each attribute: Name, Type, Description) + +3. Document each of the Capabilities and Bindings + +Include the following information: + +- Capability Name, e.g. `GetObject`, `PutObject` (it maps 1:1 with an AWS API) +- Constraints (e.g. `Key`) +- IAM Policies (how the capability maps to an IAM Policy, e.g. Effect: Allow, Action: s3:GetObject, Resource: `arn:aws:s3:::${bucketName}/${Key}`) +- Environment Variables (what environment variables should be added to a Lambda Function so that it can access the capability, e.g. `BUCKET_NAME`, `BUCKET_ARN`, `QUEUE_URL`, `QUEUE_ARN`, etc.) + +4. Research and design each of the Lifecycle Operations + +- **Diff** - identify which properties are always stable across any update, which properties change conditionally depending on new and old values, which properties trigger a replacement. This is usually just a distinct list, but can sometimes require if-this-then-that logic. Document it explicitly and exhaustively. Cross-reference with AWS CloudFormation, Terraform Provider and Pulumi Provider docs. + +:::warning +You should almost never use `no-op` in the Diff. No-op should be explicitly designed as a way to say "i know this property changed, but i don't want it to trigger an update". This is an edge-case and not the norm. Usually you want diff to return `undefined` or `void` to let the engine apply the default update logic. Diff is usually just use as an optimization or to identify replacement instead of update. +::: + +- **Read** - determine which API calls are required to read the Output Attributes of a Resource from the Cloud Provider state (otherwise known as refresh or synchronize resource state). This is usually a single Get{Resource} API call, but can be a complex set of calls depending on the Service. Read can also be called without the current Output Attributes because of past state persistence failures. These cases are handled by computing the deterministic Physical Name and looking it up or by searching for Resources using tags (if the Cloud Provider supports it). Read may return `Unowned(attrs)` when the resource exists but lacks our ownership tags, signalling the engine to gate adoption behind `--adopt` or `adopt(true)`. +- **Pre-Create** - determine if the Resource needs a pre-create operation. This is usually only the case for the special Function/Runtime Resources like AWS Lambda Functions. If it is required, then document which API call(s) should be called and what the empty (unit) input properties are. E.g. a Lambda Function takes a simple script that exports a no-op handler function. +- **Reconcile** - determine the API calls needed to converge the cloud's actual state to the desired state described by the new Input Properties. Reconcile must be a single flow that works whether the resource is missing (greenfield create), pre-existing under our ownership (update), or freshly adopted (`output` defined but `olds` absent). See the **Reconciler doctrine** section below. +- **Delete** - determine which APIs should be called and in what order to delete an existing Resource. Delete should be idempotent so that if the resource has already been deleted, it is not considered an error. It is common for deletions to fail because of Dependency Violations or Eventual Consistency Errors. These are not always called Dependency Violations in the API docs, so attention should be paid to investigating each API's possible error codes and how they should be handled by the Delete operation. Should we retry for a period of time, indefinitely, or fail immediately? + +# Reconciler doctrine + +The provider's `reconcile` function replaces the legacy `create` + `update` pair. It runs every time the engine wants to make the cloud match the desired state — whether that's the first time the resource is being provisioned, a routine update, or a takeover after `read` returned an existing cloud resource. + +It receives `output: Attributes | undefined` and `olds: Props | undefined`: + +| `output` | `olds` | Meaning | +| ------------ | ------------ | ------------------------------------------------- | +| `undefined` | `undefined` | Greenfield — no prior physical resource | +| defined | defined | Routine update — engine-owned resource | +| defined | `undefined` | Adoption — engine adopted via `read` | + +A reconciler MUST work correctly for all three combinations. It MUST NOT branch the body on `output === undefined` and run different code paths for "create" vs "update". That pattern is just rename-and-branch and re-introduces every assumption the old `create`/`update` split made. Instead, write one flow: + +``` +1. Observe — derive the physical identifier; read live cloud state via getX/describeX +2. Ensure — if the resource is missing, call createX. Catch AlreadyExists/ConflictException + as a race and continue. Wait for active state if applicable. +3. Sync — for each mutable aspect (settings, sub-resources, tags, policy): + - read OBSERVED cloud state (not olds) + - compute desired state from news + bindings + - diff observed against desired + - apply only the delta API call (skip the API entirely on no-op) +4. Return — re-read final state if needed; return the fresh Attributes shape +``` + +Key invariants: + +- **Observation > assumption.** Cloud state is authoritative. `olds` is at most a hint to skip a no-op API call; it is never the source of truth for what's actually deployed. +- **Each sync step is independently idempotent.** Crash mid-reconcile, re-run, you converge. +- **`output` is treated as a cache** for stable identifiers (physical name, ARN, immutable id). It is NOT a guarantee that the resource still exists. If it doesn't, observation falls through to "missing" and ensure recreates. +- **`AlreadyExists`/`NotFoundException`/`ResourceInUseException`-style errors are caught**, not propagated — they're races or eventual-consistency, not failures. +- **Tags use observed cloud tags as the diff baseline**, not `olds.tags` or `output.tags`. Adoption may bring you a resource with foreign tags that need to be reconciled. + +:::warning +**Do not write `if (output === undefined) { /* create body */ } else { /* update body */ }`.** That is rename-and-branch, not reconciliation. The reconciler's body is one observe-ensure-sync flow that produces correct cloud state regardless of starting point. +::: + +The canonical reference reconcilers cover the common shapes: + +- [S3 Bucket](./packages/alchemy/src/AWS/S3/Bucket.ts) — uses `ensureBucketExists` + `syncBucketTags` + `syncBucketPolicy` helpers; each helper is itself a tiny reconciler. +- [SQS Queue](./packages/alchemy/src/AWS/SQS/Queue.ts) — observe via `getQueueUrl`, ensure via `createQueue` (tolerates `QueueNameExists` race), sync attributes by diffing `getQueueAttributes` against desired, sync tags. +- [Kinesis Stream](./packages/alchemy/src/AWS/Kinesis/Stream.ts) — many mutable aspects (mode, shards, retention, encryption, metrics), each its own observed-vs-desired sync block. +- [DynamoDB Table](./packages/alchemy/src/AWS/DynamoDB/Table.ts) — multi-API observation (table + tags + PITR + TTL), per-aspect diffing, GSI delta application. +- [EC2 Vpc](./packages/alchemy/src/AWS/EC2/Vpc.ts) — auto-assigned id, observe via `describeVpcs([output.vpcId])` with NotFound fallback to create, sync DNS attrs by reading `describeVpcAttribute`, sync tags from observed `vpc.Tags`. +- [Lambda Function](./packages/alchemy/src/AWS/Lambda/Function.ts) — uses `createOrUpdateFunction` / `createOrUpdateFunctionUrl` / `attachBindings` helpers, each idempotent. +- [Cloudflare Worker](./packages/alchemy/src/Cloudflare/Workers/Worker.ts) — non-AWS API; the underlying `putWorker` is a true upsert, so reconcile observes existing settings and delegates. + +Existence-only resources (Lambda Permission, EC2 Route, EC2 RouteTableAssociation, IAM AccessKey, etc.) have nothing mutable beyond their identity. Their reconciler is just observe → if missing, create. There is no sync step. + +5. Research and design the test cases for each resource. Test cases can be single or multi-step. Single-step test cases are just testing a single create success or failure mode. Multi-step cases are testing a sequence of operations, starting with create and then updating or replacing the resource multiple times. Test cases should be designed to be exhaustive and cover all possible success and failure modes, starting from simple happy paths to long, complicated aggregate (including other resources) smoke tests. +6. Implement the Resource contract and Provider in `packages/alchemy/src/{Cloud}/{Service}/{Resource}.ts`. + +The Resource contract (Props, Attributes, Binding Contract) and the Resource Provider (lifecycle operations) are co-located in the same file. + +Read through the established examples to understand the pattern: + +- [S3 Bucket](./packages/alchemy/src/AWS/S3/Bucket.ts) +- [SQS Queue](./packages/alchemy/src/AWS/SQS/Queue.ts) +- [DynamoDB Table](./packages/alchemy/src/AWS/DynamoDB/Table.ts) +- [Kinesis Stream](./packages/alchemy/src/AWS/Kinesis/Stream.ts) +- [Lambda Function](./packages/alchemy/src/AWS/Lambda/Function.ts) +- [VPC](./packages/alchemy/src/AWS/EC2/Vpc.ts) +- [Subnet](./packages/alchemy/src/AWS/EC2/Subnet.ts) + +The Resource interface takes four type parameters: `Resource`. + +```ts +export interface Stream extends Resource< + "AWS.Kinesis.Stream", + StreamProps, + { + streamName: string; + streamArn: string; + streamStatus: StreamStatus; + } +> {} + +export const Stream = Resource("AWS.Kinesis.Stream"); +``` + +For Resources that accept Bindings (like Lambda Function), include a fourth type parameter for the Binding Contract: + +```ts +export interface Function extends Resource< + "AWS.Lambda.Function", + FunctionProps, + { + functionArn: string; + functionName: string; + functionUrl: string | undefined; + roleName: string; + roleArn: string; + }, + { + env?: Record; + policyStatements?: PolicyStatement[]; + } +> {} +``` + +:::tip +Some Input Property types are wrapped in an `Input`, but not all are. Only properties that may need to be references to another resource's Output Attribute. E.g. common use-cases are `Input`, `Input`, `Tags: Record>`. +::: + +:::warning +For fields like `name: string`, `bucketName: string`, `bucketPrefix: string`, you should not use `Input` because these properties need to be statically knowable in the `diff` function. +::: + +7. Implement the Capabilities as `Binding.Service` + `Binding.Policy` pairs in `packages/alchemy/src/{Cloud}/{Service}/{Capability}.ts`. + +Each capability has two parts: + +- **`Binding.Service`** — runtime SDK wrapper, provided on the Function Effect (bundled into Lambda/Worker) +- **`Binding.Policy`** — deploy-time IAM policy attachment, provided on the Stack via `AWS.providers()()` (never bundled) + +Read through the established capabilities to understand the pattern: + +- [S3 GetObject](./packages/alchemy/src/AWS/S3/GetObject.ts) — `Binding.Service` + `Binding.Policy` +- [S3 PutObject](./packages/alchemy/src/AWS/S3/PutObject.ts) — `Binding.Service` + `Binding.Policy` +- [SQS SendMessage](./packages/alchemy/src/AWS/SQS/SendMessage.ts) — `Binding.Service` + `Binding.Policy` +- [DynamoDB GetItem](./packages/alchemy/src/AWS/DynamoDB/GetItem.ts) — `Binding.Service` + `Binding.Policy` +- [Kinesis PutRecord](./packages/alchemy/src/AWS/Kinesis/PutRecord.ts) — `Binding.Service` + `Binding.Policy` +- [Lambda InvokeFunction](./packages/alchemy/src/AWS/Lambda/InvokeFunction.ts) — `Binding.Service` + `Binding.Policy` + +For Event Sources, see: + +- [SQS QueueEventSource](./packages/alchemy/src/AWS/SQS/QueueEventSource.ts) +- [S3 BucketEventSource](./packages/alchemy/src/AWS/S3/BucketEventSource.ts) + +The `Binding.Policy` implementation calls `ctx.bind({ policyStatements: [...] })` on the target Function, which records binding data on the Stack. The `Binding.Service` implementation resolves the Policy via `yield* Policy(resource)`, then returns a typed callable that wraps the SDK client. At runtime, the Policy is not provided and becomes a no-op. + +Each capability exports four things: + +```ts +// 1. The Binding.Service class +export class PutRecord extends Binding.Service<...>()("AWS.Kinesis.PutRecord") {} + +// 2. The Binding.Service Live layer (provided on Function Effect) +export const PutRecordLive = Layer.effect(PutRecord, ...); + +// 3. The Binding.Policy class +export class PutRecordPolicy extends Binding.Policy<...>()("AWS.Kinesis.PutRecord") {} + +// 4. The Binding.Policy Live layer (provided on Stack via AWS.providers()()) +export const PutRecordPolicyLive = Layer.effect(PutRecordPolicy, ...); +``` + +### Runtime-only methods: color with `Alchemy.RuntimeContext` + +The runtime callable returned by a `Binding.Service` (the inner Effect inside `.bind(resource)`'s return) **must** declare `Alchemy.RuntimeContext` as a requirement. This is how Alchemy models "this code can only run inside a deployed Function/Worker" at the type level — analogous to a colored function. + +```ts +import type { RuntimeContext } from "../../RuntimeContext.ts"; + +export class GetItem extends Binding.Service< + GetItem, + ( + table: T, + ) => Effect.Effect< + ( + request: GetItemRequest, + ) => Effect.Effect< + DynamoDB.GetItemOutput, + DynamoDB.GetItemError, + RuntimeContext // ← runtime-only + > + > +>()("AWS.DynamoDB.GetItem") {} +``` + +Rules: + +- **Outer Effect** (the `bind(resource)` setup) runs at the Function's init phase. It does NOT require `RuntimeContext`. +- **Inner Effect** (the actual SDK invocation) only makes sense inside a running Function. It MUST require `RuntimeContext`. +- Resolve cloud-environment services (`WorkerEnvironment`, AWS SDK clients, etc.) once during Layer construction and close over them. Do NOT leak `WorkerEnvironment` / `Lambda.FunctionEnvironment` onto the runtime callable — that couples downstream service code to a specific cloud and breaks Layer encapsulation. The Function/Worker runtime satisfies `RuntimeContext` automatically. +- The implementation can return `Effect.Effect` without explicitly providing `RuntimeContext` (it's contravariant in `R`); just declare it on the interface. + +Why this matters: consumers can build cloud-agnostic services on top of bindings using `Layer.effect(Tag, ...)` without polluting their service interface with `WorkerEnvironment`. See [Layers concept](./website/src/content/docs/concepts/layers.mdx). + +After implementing, register the Policy in `AWS.providers()()`: + +- Add the `*PolicyLive` layer to `bindings()` in [Providers.ts](./packages/alchemy/src/AWS/Providers.ts) +- Re-export from the service's `index.ts` + +:::tip +If you need to know what AWS region or account ID the resource is being created/updated in, you can use this inside any of the lifecycle operations. + +```ts +const region = yield * Region; +const account = yield * Account; +``` + +::: + +:::warning +You should favor getting the region/account INSIDE the lifecycle operations instead of inside the Layer effect like this because then it's scoped to the resource isntead of the resource provider: + +```ts +reconcile: Effect.fn(function* ({ id, news, output, session }) { + const region = yield* Region; + const accountId = yield* Account; +}); +``` + +::: + +:::warning +Do not use `Effect.orDie` in the lifecycle operations since this will crash the whole IaC engine. +::: + +:::warning +**Never use `async`/`await`, raw `Promise`, `node:fs/promises`, `node:fs`, `node:os`, or `pathe` directly in resource code.** Always use the Effect platform services so that effects remain composable, traceable, retryable, and testable: + +| Don't | Do | +| ---------------------------------------------------- | ----------------------------------------------------------- | +| `import fs from "node:fs/promises"` | `const fs = yield* FileSystem.FileSystem` | +| `await fs.readFile(p, "utf8")` | `yield* fs.readFileString(p)` | +| `await fs.mkdtemp(...)` | `yield* fs.makeTempDirectory({ prefix: ... })` | +| `import path from "pathe"` / `node:path` | `const path = yield* Path.Path` | +| `await fetch(...)` | `yield* HttpClient.HttpClient` + `HttpClientRequest` | +| `Effect.promise(() => listSqlFiles(dir))` | Make `listSqlFiles` itself return `Effect` and `yield*` it | +| `new Promise((res) => setTimeout(res, ms))` | `yield* Effect.sleep(Duration.millis(ms))` | + +Sync, CPU-only Node APIs (e.g. `crypto.createHash().update().digest()`, `process.cwd()`, `Buffer`, `TextEncoder`) must still be wrapped in `Effect.sync(() => ...)` (or `Effect.try` if they can throw) so the call participates in the Effect runtime — tracing, interruption, and error channels. Don't call them as bare expressions inside `Effect.gen`. + +```ts +const hash = yield* Effect.sync(() => + crypto.createHash("sha256").update(input).digest("hex"), +); +const cwd = yield* Effect.sync(() => process.cwd()); +``` + +This applies to **lifecycle operations, helpers, AND tests**. Tests must use `FileSystem.FileSystem`/`Path.Path` for any file/path access (see [Database.test.ts](./packages/alchemy/test/Cloudflare/D1/Database.test.ts) for the pattern). +::: + +:::tip +If a Resource supports tags, you should always include the internal Alchemy tags to brand the resource with the app, stage and logical ID so that we can "know" that we created it and are responsible for it. + +```ts +reconcile: Effect.fn(function* ({ id, news, output, session }) { + const internalTags = yield* createInternalTags(id); + const userTags = news.tags ?? {}; + const allTags = { ...internalTags, ...userTags }; +}); +``` + +::: + +:::warning +Do not roll your own tag diffing logic, always use `diffTags` from [Tags.ts](./packages/alchemy/src/Tags.ts), and diff against **observed cloud tags** (not `olds.tags` or `output.tags`). Adoption can hand you a resource whose tags don't match what we last persisted. + +```ts +reconcile: Effect.fn(function* ({ id, news, output, session }) { + const internalTags = yield* createInternalTags(id); + const newTags = { ...news.tags, ...internalTags }; + // Read tags fresh from the cloud so adoption (where tags may not match + // what we last persisted) converges correctly. + const oldTags = yield* fetchObservedTags(/* … */); + // Option 1. use `upsert` if the API expects you to create/update tags in one call + const { removed, upsert } = diffTags(oldTags, newTags); + // Option 2. use `added` and `updated` if the API expects you to create/update tags in separate calls + const { removed, added, updated } = diffTags(oldTags, newTags); + // Option 3. use `upsert` only if the API doesn't expect you to remove tags (only PUT/UPDATe) + const { upsert } = diffTags(oldTags, newTags); +``` + +::: + +9. Implement the test cases in `packages/alchemy/test/{Cloud}/{Service}/{Resource}.test.ts`. + +Read through the established test cases before continuing so that you understand the pattern and structure of the test cases. + +- [S3 Bucket Test Cases](./test/AWS/S3/Bucket.test.ts) +- [SQS Queue Test Cases](./test/AWS/SQS/Queue.test.ts) +- [Lambda Function Test Cases](./test/AWS/Lambda/Function.test.ts) +- [Kinesis Stream Test Cases](./test/AWS/Kinesis/Stream.test.ts) +- [DynamoDB Table Test Cases](./test/AWS/DynamoDB/Table.test.ts) +- [VPC Test Cases](./test/AWS/EC2/Vpc.test.ts) +- [Subnet Test Cases](./test/AWS/EC2/Subnet.test.ts) + +:::warning +Never use `Date.now()` when constructing the physical name of a resource. You should either: + +1. Do not proide a name and rely on the resource provider to generate a unique name for you from the app, stage and logical ID. +2. Construct a deterministic one unique to each test case. But it should be the same on each subsequent run of the test case. + ::: + +3. Consider implementing an aggregate Smoke test that brings together multiple resources that are often used together. + +See the [VPC Smoke Test](./test/AWS/EC2/Vpc.smoke.test.ts) for an example. + +11. Add the resource-level JSDoc (`@section` + `@example` blocks) and field-level JSDoc on each prop/attribute on the source `.ts` file. Then run `bun generate:api-reference` to refresh `website/src/content/docs/providers/{Cloud}/{Resource}.md`. Do NOT manually edit the generated markdown. + +# Test Fixtures for Effect-Native Workers / Functions + +To test runtime behavior of an Effect-native Worker, Workflow, Lambda, etc., write a **fixture** that defines the Worker/Function with the bindings under test and exposes one HTTP route per behavior, then write a **test** that deploys the fixture once via `beforeAll` and drives it over HTTP. + +## File system layout + +Put fixtures in a `fixtures/` directory next to the test file. Each test suite owns its own fixtures — never reach across suites: + +```sh +packages/alchemy/test/{Cloud}/{Service}/{Resource}.test.ts +packages/alchemy/test/{Cloud}/{Service}/fixtures/{worker|workflow|handler}.ts +``` + +## Fixture shape + +Resolve the bindings, expose one route per behavior, default-export the class so the test can deploy it directly: + +```ts +// fixtures/worker.ts +import * as Cloudflare from "@/Cloudflare/index.ts"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Gateway } from "./gateway.ts"; + +export default class TestWorker extends Cloudflare.Worker()( + "TestWorker", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const aiGateway = yield* Cloudflare.AiGateway.bind(Gateway); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + if (request.url.startsWith("/url")) { + const url = yield* aiGateway.getUrl().pipe(Effect.orDie); + return yield* HttpServerResponse.json({ url }); + } + return HttpServerResponse.text("ok"); + }), + }; + }).pipe(Effect.provide(Cloudflare.AiGatewayBindingLive)), +) {} +``` + +## Test shape + +Compose a `Stack` that deploys the fixture, share one deploy across the file with `beforeAll`/`afterAll`, drive it via `HttpClient`, and retry the first request through edge propagation: + +```ts +// Service.test.ts +import * as Alchemy from "@/index.ts"; +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import TestWorker from "./fixtures/worker.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const Stack = Alchemy.Stack( + "ServiceTestStack", + { providers: Cloudflare.providers(), state: Cloudflare.state() }, + Effect.gen(function* () { + const worker = yield* TestWorker; + return { url: worker.url.as() }; + }), +); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "deployed worker exercises the binding", + Effect.gen(function* () { + const { url } = yield* stack; + const client = yield* HttpClient.HttpClient; + + const res = yield* client.get(`${url}/url`).pipe( + Effect.retry({ schedule: Schedule.exponential("500 millis"), times: 10 }), + ); + expect(res.status).toBe(200); + const body = (yield* res.json) as { url: string }; + expect(body.url).toContain("gateway.ai.cloudflare.com"); + }), + { timeout: 180_000 }, +); +``` + +Notes: + +- `Test.make({ providers: Cloudflare.providers() })` gives you `test`, `beforeAll`, `afterAll`, `deploy`, `destroy`. +- `beforeAll(deploy(Stack))` returns a handle (`stack` above) that every `test` body can `yield*` to get the stack outputs. +- `afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack))` is the standard cleanup — set `NO_DESTROY=1` locally to keep the deployment around between runs while iterating. +- Always retry the first request (`Schedule.exponential("500 millis")`) — fresh workers.dev URLs and Lambda function URLs take a few seconds to start serving 200s. +- For POST: use `client.post(url)` for empty bodies, or `HttpClient.execute(HttpClientRequest.post(url).pipe(HttpClientRequest.bodyJsonUnsafe(body)))` for typed bodies. +- **Never use `while (Date.now() < deadline)` loops to poll** for an async side effect (a workflow status, a cron fire, a queue drain, eventual-consistency read, etc.). Use `Effect.repeat` with a `Schedule` and an `until` predicate so the polling participates in the Effect runtime — tracing, interruption, and error propagation work correctly, and the intent is declarative. Cap iterations with `times: N` (or a bounded schedule) so the test fails fast instead of running until the vitest timeout: + + ```ts + // good — declarative, bounded, interruption-safe + const value = yield* fetchValue.pipe( + Effect.repeat({ + schedule: Schedule.spaced("5 seconds"), + until: (v) => v.ready, + times: 36, + }), + ); + + // bad — opaque loop, ignores interruption, leaks into vitest timeout + let value: Value | undefined; + const deadline = Date.now() + 180_000; + while (Date.now() < deadline) { + value = yield* fetchValue; + if (value.ready) break; + yield* Effect.sleep("5 seconds"); + } + ``` + + See [CronEventSource.test.ts](./packages/alchemy/test/Cloudflare/Workers/CronEventSource.test.ts) for a real-world example (polling a DO via the worker's `/times` route until the cron handler fires). + +## Reference implementations + +- Cloudflare AiGateway — [worker fixture](./packages/alchemy/test/Cloudflare/AiGateway/worker.ts) + [test](./packages/alchemy/test/Cloudflare/AiGateway/AiGateway.test.ts) (the deploy+fetch case lives at the bottom of the file) +- Cloudflare D1Connection — [worker fixture](./packages/alchemy/test/Cloudflare/D1/d1-worker.ts) + [test](./packages/alchemy/test/Cloudflare/D1/D1Binding.test.ts) +- Cloudflare Workflow — [workflow fixture](./packages/alchemy/test/Cloudflare/Workers/fixtures/test-workflow.ts) + [worker fixture](./packages/alchemy/test/Cloudflare/Workers/fixtures/workflow-worker.ts) + [test](./packages/alchemy/test/Cloudflare/Workers/Workflow.test.ts) +- Cloudflare Cron Trigger — [worker + DO fixture](./packages/alchemy/test/Cloudflare/Workers/fixtures/cron-worker.ts) + [test](./packages/alchemy/test/Cloudflare/Workers/CronEventSource.test.ts) (cron handler writes to a DO; test polls a fetch route with `Effect.repeat` until the scheduled handler fires) +- Cloudflare Images — [effect fixture](./packages/alchemy/test/Cloudflare/Images/fixtures/effect-worker.ts) + [async fixture](./packages/alchemy/test/Cloudflare/Images/fixtures/async-worker.ts) + [test](./packages/alchemy/test/Cloudflare/Images/Images.test.ts) +- AWS Lambda (DynamoDB bindings) — [Lambda fixture](./packages/alchemy/test/AWS/DynamoDB/handler.ts) + [test](./packages/alchemy/test/AWS/DynamoDB/Bindings.test.ts) (one `describe("")` per binding, all driving the same deployed Lambda) + +# Spec-Driven Service Bring-Up + +Use @processes/AWS.md as the source of truth for bringing a single AWS service from zero to full coverage. + +That process covers: + +- deriving resources, bindings, event sources, and helpers from distilled +- the audit-driven implementation loop +- deterministic checks for registration and binding test coverage +- Lambda fixture testing conventions +- learned conventions like no auto-marshalling and one `describe("")` block per binding + +Keep `AGENTS.md` high-level and update @processes/AWS.md when the process evolves. + +When a canonical resource needs mutable event-source configuration and there is any chance of circularity, prefer a resource binding contract over a plain input prop. DynamoDB Streams is the reference case: `Table` owns the actual stream state, while `streams(table)` injects that state via bindings and the runtime-specific layer handles the subscription mechanics. See @processes/AWS.md for the DynamoDB Streams case study. + +# Build and Type Checking + +Always run type checking before committing changes: + +```bash +bun tsc -b +``` + +This runs the TypeScript compiler in build mode, which checks all projects in the workspace. This is critical because CI will fail if there are type errors. + +## Build Commands + +| Command | Description | +| ----------------- | -------------------------------------------------------------------------------------------- | +| `bun tsc -b` | Type check all projects (always run before committing) | +| `bun run build` | Clean, type check, and build the alchemy package | +| `bun build:clean` | Full clean rebuild: cleans all artifacts, reinstalls dependencies, builds, and downloads env | + +Use `bun build:clean` when you encounter stale build artifacts or dependency issues. It runs: + +1. `bun clean .` - Removes all untracked files except .env +2. `bun i` - Reinstalls dependencies +3. `bun run build` - Builds the project +4. `bun download:env` - Downloads environment files + +# Tutorial Documentation Standard + +Tutorials under `website/src/content/docs/tutorial/` are **step-by-step and granular**: every code snippet introduces exactly **one** new thing, followed by a short prose explanation of just that thing. Each step gets its own `##` heading. + +**Anti-pattern** — one snippet that adds multiple distinct changes, followed by a numbered list or bullet list explaining each: + +````md +## Bind the DO to the Worker + +```diff lang="typescript" ++import Counter from "./counter.ts"; ++import { HttpServerRequest } from "..."; + + Effect.gen(function* () { ++ const counters = yield* Counter; + return { + fetch: Effect.gen(function* () { ++ const request = yield* HttpServerRequest; ++ if (request.url.startsWith("/counter/") && ...) { ++ const next = yield* counters.getByName(name).increment(); ++ return HttpServerResponse.text(String(next)); ++ } + return HttpServerResponse.text("Hello!"); + }), + }; + }) +``` + +Two things just happened: +1. `yield* Counter` registers the DO ... +2. `counters.getByName(name)` returns a typed stub ... +```` + +**Correct** — split into one heading per step, each with one snippet and one explanation: + +````md +## Bind the DO to the Worker + +```diff lang="typescript" ++import Counter from "./counter.ts"; + + Effect.gen(function* () { ++ const counters = yield* Counter; + ... + }) +``` + +`yield* Counter` registers the DO with the Worker (binding + class-migration metadata) and hands you the namespace. + +## Call the DO from `fetch` + +```diff lang="typescript" ++import { HttpServerRequest } from "..."; + + fetch: Effect.gen(function* () { ++ const request = yield* HttpServerRequest; ++ if (request.url.startsWith("/counter/") && ...) { ++ const next = yield* counters.getByName(name).increment(); ++ return HttpServerResponse.text(String(next)); ++ } + return HttpServerResponse.text("Hello!"); + }) +``` + +`counters.getByName(name)` returns a typed stub — `increment()` and `get()` round-trip through Cloudflare's RPC machinery. +```` + +Rules of thumb: + +- If you find yourself writing "Two/three things just happened", "A few things are happening here", or a numbered/bulleted list explaining separate parts of a single snippet — **split the snippet**. +- One concept ⇒ one heading ⇒ one diff snippet ⇒ one explanation paragraph (no bullets). +- Bullet/numbered lists are fine when they describe a recap, prerequisites, or genuinely list-shaped content (e.g. "the Worker now handles two routes: PUT and GET" at the end). They are **not** fine as a substitute for splitting a compound snippet. +- A single API call that internally does several things (e.g. `Cloudflare.upgrade()`) doesn't need splitting — describe its behavior in prose. +- Use `diff lang="typescript"` blocks so each step shows what's added on top of the previous step. + +# Pull Request Conventions + +When you automatically open a PR, it MUST follow this structure: + +- **Title**: Use conventional commit format (e.g. `fix(website): mobile theme metas`, `feat(aws/s3): add bucket lifecycle rules`). +- **Description heading levels**: NEVER use `#` or `##` in the PR description. The smallest heading allowed is `###`. The PR description must NOT begin with its own title heading — GitHub already renders the PR title above it. +- **Content**: Aim for the minimal content needed to convey the idea. + - Use simple sentences. If there are multiple discrete changes, use bullet points. + - **Prefer code snippets over prose.** A short ` ```ts ` or ` ```diff ` block showing the new/changed shape is worth more than a paragraph explaining it. Reach for code first; only add prose to fill in the "why" the snippet can't show on its own. + - Be direct and succinct. Cut adjectives, justifications, and anything that reads like marketing copy. If a sentence is restating what the diff already shows, delete it. + - **Never include a "Test plan", "Testing", or checklist of TODOs.** PR descriptions document the change, not the verification process. If something needs manual verification, follow the draft-PR rule below. + - Skip examples for trivial fixes, internal refactors, or doc-only changes. + +Example PR description (good — code snippet does the talking): + +```` +Track which state-store backend each project uses by emitting a `state_store.init` span tagged with `alchemy.state_store.kind`. + +```ts +// every Layer.effect(State, …) site now wraps construction: +makeLocalState().pipe(recordStateStoreInit("local")) +``` + +Dashboard groups projects by kind from these spans (Axiom can't APL-query metric datasets). +```` +- **Outstanding work / testing / review needed**: If there are outstanding steps, manual testing required, or review items, DO NOT leave a comment on the PR and DO NOT include them in the PR description. Instead: + 1. Mark the PR as **draft**. + 2. Tell the user (in the chat that initiated the PR creation) what is outstanding. + +:::warning +**Markdown content must reach GitHub verbatim** — un-escaped backticks, fenced code blocks, etc. The reliable shape is to write the description to a file and pass `--body-file ` to `gh pr create` / `gh pr edit`: + +```sh +# write the body to a temp file (use Write tool, not echo/cat heredoc) +gh pr edit 179 --body-file /tmp/pr-body.md +``` + +Do **not** inline the body via `--body "$(cat <<'EOF' ... EOF)"`. Even with a single-quoted heredoc some shells / `gh` versions still mangle backticks and backslashes; the resulting PR body ends up with literal `\`` sequences instead of inline code spans. `--body-file` sidesteps shell quoting entirely. + +If you need to update an already-created PR's body, prefer `gh pr edit --body-file ...`. If that silently no-ops (older `gh` versions), fall back to `gh api -X PATCH repos///pulls/ -F body=@/tmp/pr-body.md`. +::: + +The summary goes at the very top of the description as plain prose — NO heading above it, no `### Summary`, nothing. The PR title already serves as the title; do not repeat or re-title it. Only add `###` subheadings further down if the description genuinely has multiple sections worth separating. + +Example PR description (good): + +``` +Persist the user's selected theme across reloads and fix a hero scroll glitch on mobile. + +- Read theme from `localStorage` on mount before first paint +- Add `` per theme so mobile chrome matches +``` + +Example PR description (BAD — do not do this): + +``` +## Theme persistence fix ← no, the PR title already exists +### Summary ← no, summary needs no heading +Persist the user's theme... +``` diff --git a/.repos/alchemy-effect/CHANGELOG.md b/.repos/alchemy-effect/CHANGELOG.md new file mode 100644 index 00000000000..5d2ebfce521 --- /dev/null +++ b/.repos/alchemy-effect/CHANGELOG.md @@ -0,0 +1,1322 @@ +## v2.0.0-beta.49 + +###    🚀 Features + +- **cloudflare**: DNS record bindings  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/511 [(fafc9)](https://github.com/alchemy-run/alchemy-effect/commit/fafc9e9c) +- **output**: Add flatMap and method-style combinators  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/512 [(00234)](https://github.com/alchemy-run/alchemy-effect/commit/002343f0) + +###    🐞 Bug Fixes + +- Don't unwrap unredacted values  -  by **sam** [(f4ef0)](https://github.com/alchemy-run/alchemy-effect/commit/f4ef0381) +- **cloudflare**: Ensure all yielded Config in a Worker to use secret_text  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/510 [(38c62)](https://github.com/alchemy-run/alchemy-effect/commit/38c62b6b) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.48...HEAD) + +--- + +## v2.0.0-beta.48 + +###    🚀 Features + +- **cli**: + - Add cloudflare create-token command  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/496 [(ab886)](https://github.com/alchemy-run/alchemy-effect/commit/ab8864fe) +- **cloudflare**: + - Zone  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/493 [(38b2f)](https://github.com/alchemy-run/alchemy-effect/commit/38b2f0a8) + - Rename BrowserRendering to Browser  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/503 [(1a62d)](https://github.com/alchemy-run/alchemy-effect/commit/1a62d247) + - Tunnel Bindings  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/508 [(86bcf)](https://github.com/alchemy-run/alchemy-effect/commit/86bcf260) + - **browser**: Yieldable BrowserRendering marker  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/498 [(05283)](https://github.com/alchemy-run/alchemy-effect/commit/052832a8) + - **images**: Yieldable Images binding  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/501 [(46c15)](https://github.com/alchemy-run/alchemy-effect/commit/46c158a6) + - **ratelimit**: Add RateLimit binding  -  by **Alex** and **sam** in https://github.com/alchemy-run/alchemy-effect/issues/238 [(eb8f5)](https://github.com/alchemy-run/alchemy-effect/commit/eb8f5efc) + - **workers**: Yieldable DynamicWorkerLoader marker  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/505 [(362fc)](https://github.com/alchemy-run/alchemy-effect/commit/362fcdff) +- **docs**: + - Add Bindings section and refocus Providers on lifecycle interface  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/502 [(a5fc3)](https://github.com/alchemy-run/alchemy-effect/commit/a5fc3412) + +###    🐞 Bug Fixes + +- **cloudflare**: + - **zone**: Use ZoneAlreadyExists tag; bump distilled 0.22.3  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/507 [(d948f)](https://github.com/alchemy-run/alchemy-effect/commit/d948f304) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.47...HEAD) + +--- + +## v2.0.0-beta.47 + +###    🚀 Features + +- **cloudflare**: + - **ai-gateway**: Add LanguageModel + AI Gateway client surface  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/389 [(51150)](https://github.com/alchemy-run/alchemy-effect/commit/51150a8c) + +###    🐞 Bug Fixes + +- **cli**: + - Skip evalStack in login and defer State store init to runtime  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/492 [(949b3)](https://github.com/alchemy-run/alchemy-effect/commit/949b3079) +- **cloudflare**: + - Handle props.env in local workers  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/473 [(0b779)](https://github.com/alchemy-run/alchemy-effect/commit/0b779dfe) + - Stream RPC return values across durable object boundary  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/478 [(6811d)](https://github.com/alchemy-run/alchemy-effect/commit/6811de57) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.46...HEAD) + +--- + +## v2.0.0-beta.46 + +###    🚀 Features + +- **cloudflare**: + - **vectorize**: Add Vectorize index as a bindable resource  -  by **David J. Felix** and **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/407 [(2089d)](https://github.com/alchemy-run/alchemy-effect/commit/2089dbf7) + +###    🐞 Bug Fixes + +- **cloudflare**: Use remote state when updating Cloudflare State Store  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/477 [(a1a81)](https://github.com/alchemy-run/alchemy-effect/commit/a1a811e1) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.45...HEAD) + +--- + +## v2.0.0-beta.45 + +###    🚀 Features + +- **Random**: + - Add KeyPair resource  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/441 [(aa776)](https://github.com/alchemy-run/alchemy-effect/commit/aa776a6d) +- **aws**: + - **lambda**: Add timeout to FunctionProps  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/440 [(38a32)](https://github.com/alchemy-run/alchemy-effect/commit/38a32704) +- **cloudflare**: + - Add Browser Rendering binding  -  by **Alex** and **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/372 [(88e95)](https://github.com/alchemy-run/alchemy-effect/commit/88e95867) + - Cross-script durable object binding  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/435 [(438fb)](https://github.com/alchemy-run/alchemy-effect/commit/438fbfe1) + - RpcWorker  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/387 [(d15f0)](https://github.com/alchemy-run/alchemy-effect/commit/d15f0a9f) + - Add RpcDurableObjectNamespace + modular RpcWorker/DO  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/388 [(c801d)](https://github.com/alchemy-run/alchemy-effect/commit/c801dfd3) + - Workflows in `alchemy dev`  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/449 [(0351d)](https://github.com/alchemy-run/alchemy-effect/commit/0351d715) + - Worker.url now tries to infer canonical url & domain order is preserved  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/432 [(ae0a1)](https://github.com/alchemy-run/alchemy-effect/commit/ae0a19a6) + - AnalyticsEngine and SendEmail bindings in alchemy dev  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/460 [(aa890)](https://github.com/alchemy-run/alchemy-effect/commit/aa890310) + - Custom ports in alchemy dev  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/469 [(68ef9)](https://github.com/alchemy-run/alchemy-effect/commit/68ef90fe) + - **queue**: Accept Duration.Input for messages() time props  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/443 [(f15c0)](https://github.com/alchemy-run/alchemy-effect/commit/f15c0c50) + - **workers**: Collapse WorkerProps.bindings into WorkerProps.env  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/446 [(a0358)](https://github.com/alchemy-run/alchemy-effect/commit/a0358684) +- **core**: + - Replace Alchemy.Secret and Alchemy.Variable with effect/Config  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/445 [(53e7b)](https://github.com/alchemy-run/alchemy-effect/commit/53e7b9c5) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Update Secrets reconcilation logic to update the Secret and update distilled  -  by **Sam Goodwin** [(5d2d8)](https://github.com/alchemy-run/alchemy-effect/commit/5d2d8daf) + - Wait for queue consumer to be bound  -  by **Sam Goodwin** [(c74a8)](https://github.com/alchemy-run/alchemy-effect/commit/c74a871b) +- **better-auth**: + - Better auth as peer dep  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/434 [(08244)](https://github.com/alchemy-run/alchemy-effect/commit/08244a13) +- **cli**: + - Disable --experimental-transform-types on node.js 26  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/458 [(70359)](https://github.com/alchemy-run/alchemy-effect/commit/703591ba) + - Import stack via file URL on Windows  -  by **d3lay** in https://github.com/alchemy-run/alchemy-effect/issues/426 [(51f6f)](https://github.com/alchemy-run/alchemy-effect/commit/51f6f9e4) + - Ensure auth providers don't run in parallel and only run once  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/461 [(2421c)](https://github.com/alchemy-run/alchemy-effect/commit/2421cfe4) +- **cloudflare**: + - Dev hyperdrive binding fails for vite/async workers  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/423 [(c83ce)](https://github.com/alchemy-run/alchemy-effect/commit/c83ce2d8) + - Pass DO scriptName through async bindings  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/437 [(3a8da)](https://github.com/alchemy-run/alchemy-effect/commit/3a8daa3d) + - Handle external require calls in vite dev  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/452 [(87cc9)](https://github.com/alchemy-run/alchemy-effect/commit/87cc9f0c) + - Cloudflare.providers() has `any` in requirements  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/456 [(81814)](https://github.com/alchemy-run/alchemy-effect/commit/818147f8) + - Reconcile Worker preview subdomain state  -  by **Dawson** and **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/285 [(dbd04)](https://github.com/alchemy-run/alchemy-effect/commit/dbd042e2) + - Handle websocket upgrade in vite dev  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/457 [(67b57)](https://github.com/alchemy-run/alchemy-effect/commit/67b57f81) + - Run HttpServer pre-response handlers correctly  -  by **Lucas Thevenet**, **Sam Goodwin** and **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/404 [(2e3d1)](https://github.com/alchemy-run/alchemy-effect/commit/2e3d176f) + - Only close Scope if it's not transferred to a Stream  -  by **Sam Goodwin** [(bb970)](https://github.com/alchemy-run/alchemy-effect/commit/bb970abd) + - Trim bearer token to workaround effect beta.73 regression  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/468 [(2d08b)](https://github.com/alchemy-run/alchemy-effect/commit/2d08bcf5) + - Reduce unnecessary updates in alchemy dev  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/455 [(ab7e7)](https://github.com/alchemy-run/alchemy-effect/commit/ab7e718b) + - **worker**: Normalize bundle entry path on windows  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/453 [(5cd26)](https://github.com/alchemy-run/alchemy-effect/commit/5cd26234) +- **core**: + - Interrupt deployments when upstream non-cylic dependency fails  -  by **Sam Goodwin** [(e22ee)](https://github.com/alchemy-run/alchemy-effect/commit/e22ee4bf) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.44...HEAD) + +--- + +## v2.0.0-beta.44 + +###    🚀 Features + +- Bundle analyzer plugin  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/405 [(28b8c)](https://github.com/alchemy-run/alchemy-effect/commit/28b8c7a4) +- **cloudflare**: + - Support Artifacts binding in dev  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/419 [(3917e)](https://github.com/alchemy-run/alchemy-effect/commit/3917ef3a) + - **zaraz**: Add ZarazConfig resource  -  by **Alex** in https://github.com/alchemy-run/alchemy-effect/issues/371 [(7a232)](https://github.com/alchemy-run/alchemy-effect/commit/7a232990) + +###    🐞 Bug Fixes + +- Node.js import resolution error from @alchemy.run/node-utils  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/406 [(4820d)](https://github.com/alchemy-run/alchemy-effect/commit/4820da51) +- **bundle**: + - Support Vite-style ?raw imports  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/411 [(0e989)](https://github.com/alchemy-run/alchemy-effect/commit/0e98917c) +- **cli**: + - Filter bun's "not in project directory" watcher warning  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/124 [(67370)](https://github.com/alchemy-run/alchemy-effect/commit/673701e6) +- **cloudflare**: + - Update cloudflare-tools to 0.6.1  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/403 [(cb68e)](https://github.com/alchemy-run/alchemy-effect/commit/cb68ef48) + - Remove extraneous logs  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/410 [(7bb5d)](https://github.com/alchemy-run/alchemy-effect/commit/7bb5d132) + - Harden local durable object handling  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/408 [(19e80)](https://github.com/alchemy-run/alchemy-effect/commit/19e80aa2) + - Update cloudflare-tools to 0.6.3  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/418 [(60826)](https://github.com/alchemy-run/alchemy-effect/commit/60826480) + - Optimize WorkerBridge imports and silence warning  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/420 [(aa9ee)](https://github.com/alchemy-run/alchemy-effect/commit/aa9ee151) +- **dev**: + - Dangling processes after dev server exits  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/382 [(6a8a0)](https://github.com/alchemy-run/alchemy-effect/commit/6a8a0258) +- **website**: + - Extract privacy.mdx style block to css file  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/398 [(fd72a)](https://github.com/alchemy-run/alchemy-effect/commit/fd72a9de) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.43...HEAD) + +--- + +## v2.0.0-beta.43 + +###    🚀 Features + +- **planetscale**: Add planetscale resources  -  by **Lucas Thevenet** and **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/113 [(ce5ea)](https://github.com/alchemy-run/alchemy-effect/commit/ce5eabbf) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Move WorkerEnvironment requirement to the Layer instead of the binding API call  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/383 [(ab3a0)](https://github.com/alchemy-run/alchemy-effect/commit/ab3a0268) + - Allow Outputs as input to Cloudflare.Worker  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/394 [(b9ea9)](https://github.com/alchemy-run/alchemy-effect/commit/b9ea9315) +- **core**: + - Remove actions when destroying  -  by **Sam Goodwin** [(d8551)](https://github.com/alchemy-run/alchemy-effect/commit/d8551e21) +- **docker**: + - Proper docker command on windows  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/377 [(d1c42)](https://github.com/alchemy-run/alchemy-effect/commit/d1c421af) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.42...HEAD) + +--- + +## v2.0.0-beta.42 + +###    🐞 Bug Fixes + +- **Cloudflare**: + - **Worker**: Defer Platform hook bindings to avoid TDZ from npm  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/378 [(1b498)](https://github.com/alchemy-run/alchemy-effect/commit/1b498227) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.41...HEAD) + +--- + +## v2.0.0-beta.41 + +###    🚀 Features + +- **Cloudflare**: Include WorkerProps.env in InferEnv  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/351 [(f157e)](https://github.com/alchemy-run/alchemy-effect/commit/f157ed89) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Fix RPC and HTTP APIs by scoping runtime effects per request  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/374 [(b7cdc)](https://github.com/alchemy-run/alchemy-effect/commit/b7cdca09) +- **cloudflare**: + - Externalize lightningcss + fsevents in worker bundler  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/363 [(5dc95)](https://github.com/alchemy-run/alchemy-effect/commit/5dc95a0e) + - Provide worker env to do methods  -  by **Dillon Mulroy** in https://github.com/alchemy-run/alchemy-effect/issues/369 [(7b5be)](https://github.com/alchemy-run/alchemy-effect/commit/7b5be8be) + - Add WorkerExecutionContext and ExecutionContext to worker fetch types  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/358 [(d2d0f)](https://github.com/alchemy-run/alchemy-effect/commit/d2d0fa8d) + - **worker**: Force delete scripts on teardown  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/348 [(f37d0)](https://github.com/alchemy-run/alchemy-effect/commit/f37d06cf) +- **core**: + - Eager terminal status events + pending while waiting on deps  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/376 [(b307e)](https://github.com/alchemy-run/alchemy-effect/commit/b307ee5a) +- **sidecar**: + - Preserve Redacted values across RPC serialization  -  by **Juliaan** in https://github.com/alchemy-run/alchemy-effect/issues/356 [(b2334)](https://github.com/alchemy-run/alchemy-effect/commit/b2334c5e) +- **state**: + - Remove that one log we don't want, if you know you know  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/347 [(ab648)](https://github.com/alchemy-run/alchemy-effect/commit/ab648e16) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.40...HEAD) + +--- + +## v2.0.0-beta.40 + +###    🚀 Features + +- **axiom**: Smartfilter chart subtype  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/339 [(f2650)](https://github.com/alchemy-run/alchemy-effect/commit/f2650d05) +- **cloudflare**: Vite dev  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/331 [(1913a)](https://github.com/alchemy-run/alchemy-effect/commit/1913aafb) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Revert scopeTransferToStream which is causing 415  -  by **Sam Goodwin** [(fe354)](https://github.com/alchemy-run/alchemy-effect/commit/fe354905) + - Bump state store version to 5  -  by **Sam Goodwin** [(baddd)](https://github.com/alchemy-run/alchemy-effect/commit/baddd845) + - **AiGateway**: + - Stop reporting update on every deploy  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/332 [(2a1f7)](https://github.com/alchemy-run/alchemy-effect/commit/2a1f7232) + - Align desired-state defaults with API defaults  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/334 [(0e3dc)](https://github.com/alchemy-run/alchemy-effect/commit/0e3dcd3b) + - **Container**: + - Forward startup options through Cloudflare.start  -  by **Christopher Yovanovitch** in https://github.com/alchemy-run/alchemy-effect/issues/341 [(1c72b)](https://github.com/alchemy-run/alchemy-effect/commit/1c72b9c0) + - **Worker**: + - Log full Cause server-side, return generic 500 to client  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/336 [(ebdbc)](https://github.com/alchemy-run/alchemy-effect/commit/ebdbc0d5) +- **aws,cloudflare**: + - Sanitize entrypoint urls  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/353 [(4cf38)](https://github.com/alchemy-run/alchemy-effect/commit/4cf3856c) +- **dev**: + - Use @alchemy.run/node-utils lockfile  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/345 [(acc8b)](https://github.com/alchemy-run/alchemy-effect/commit/acc8b885) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.39...HEAD) + +--- + +## v2.0.0-beta.39 + +###    🐞 Bug Fixes + +- **Cloudflare**: + - **Vite**: + - Inline `env` props as `import.meta.env.*` in the bundle  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/330 [(c22a1)](https://github.com/alchemy-run/alchemy-effect/commit/c22a1c7a) + - **Worker**: + - Cloudflare Worker HTTP effect lifecycle  -  by **Will King** in https://github.com/alchemy-run/alchemy-effect/issues/328 [(a1032)](https://github.com/alchemy-run/alchemy-effect/commit/a1032c34) + - Add missing SendEmail worker binding type and meta  -  by **Gerben Mulder** in https://github.com/alchemy-run/alchemy-effect/issues/326 [(a7fbb)](https://github.com/alchemy-run/alchemy-effect/commit/a7fbba51) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.38...HEAD) + +--- + +## v2.0.0-beta.38 + +###    🚀 Features + +- **cloudflare**: Add Email Routing resources and SendEmail Worker binding  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/314 [(06dab)](https://github.com/alchemy-run/alchemy-effect/commit/06dab7c5) +- **core**: Action plan node type (functions that run as part of apply)  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/315 [(8a66a)](https://github.com/alchemy-run/alchemy-effect/commit/8a66a896) + +###    🐞 Bug Fixes + +- Move to effect@4.0.0-beta.66  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/321 [(b57f7)](https://github.com/alchemy-run/alchemy-effect/commit/b57f78cf) +- Improve transport error fault tolerance  -  by **sam** [(6d67c)](https://github.com/alchemy-run/alchemy-effect/commit/6d67c24f) +- **Neon**: Use @distilled.cloud/neon SDK instead of hand-rolled API  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/270 [(1b9d4)](https://github.com/alchemy-run/alchemy-effect/commit/1b9d4372) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.37...HEAD) + +--- + +## v2.0.0-beta.37 + +###    🚀 Features + +- Cross-stack and cross-stage references  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/300 [(94ea3)](https://github.com/alchemy-run/alchemy-effect/commit/94ea3129) +- **Cloudflare**: + - Empty r2 bucket on destroy  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/276 [(2412a)](https://github.com/alchemy-run/alchemy-effect/commit/2412a3c2) + - Worker to worker binding types and tanstack start bridge example and docs  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/310 [(30dd9)](https://github.com/alchemy-run/alchemy-effect/commit/30dd99c9) + - **Workflow**: Type workflow input and output  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/304 [(1dafd)](https://github.com/alchemy-run/alchemy-effect/commit/1dafd77c) +- **cloudflare**: + - Add Worker cron triggers  -  by **Dawson** and **sam** in https://github.com/alchemy-run/alchemy-effect/issues/288 [(cae20)](https://github.com/alchemy-run/alchemy-effect/commit/cae20c98) + - Add Analytics Engine binding  -  by **Dawson** and **sam** in https://github.com/alchemy-run/alchemy-effect/issues/286 [(ba8b3)](https://github.com/alchemy-run/alchemy-effect/commit/ba8b3a16) +- **core**: + - Add Alchemy.Secret and Alchemy.Variable  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/290 [(88611)](https://github.com/alchemy-run/alchemy-effect/commit/88611eac) + +###    🐞 Bug Fixes + +- Remove deprecated libsodium wrapper types  -  by **齐天大圣** in https://github.com/alchemy-run/alchemy-effect/issues/311 [(f3f70)](https://github.com/alchemy-run/alchemy-effect/commit/f3f7079a) +- **Cloudflare**: + - **D1**: Make prepare/bind synchronous  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/299 [(6e58d)](https://github.com/alchemy-run/alchemy-effect/commit/6e58da04) +- **cloudflare**: + - Include wasm modules in local sidecar bundle  -  by **Baptiste Arnaud** and **sam** in https://github.com/alchemy-run/alchemy-effect/issues/305 [(1926d)](https://github.com/alchemy-run/alchemy-effect/commit/1926d580) +- **core**: + - Throw on JS string-coercion of unresolved Outputs  -  by **Zé Yuri** in https://github.com/alchemy-run/alchemy-effect/issues/306 [(d1b12)](https://github.com/alchemy-run/alchemy-effect/commit/d1b12ac7) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.36...HEAD) + +--- + +## v2.0.0-beta.36 + +###    🚀 Features + +- **Cloudflare**: + - **Images**: Add Images binding  -  by **Alex** in https://github.com/alchemy-run/alchemy-effect/issues/237 [(b19c3)](https://github.com/alchemy-run/alchemy-effect/commit/b19c3562) +- **cloudflare**: + - Support Hyperdrive in Worker bindings  -  by **Baptiste Arnaud** in https://github.com/alchemy-run/alchemy-effect/issues/282 [(48381)](https://github.com/alchemy-run/alchemy-effect/commit/48381a01) + - **r2**: Add lifecycleRules option to R2Bucket  -  by **Baptiste Arnaud** in https://github.com/alchemy-run/alchemy-effect/issues/284 [(ed905)](https://github.com/alchemy-run/alchemy-effect/commit/ed905d44) + +###    🐞 Bug Fixes + +- **Dev**: Resolve *.localhost via undici dispatcher  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/289 [(ae6de)](https://github.com/alchemy-run/alchemy-effect/commit/ae6de171) + +###    🏎 Performance + +- **smoke**: Batch canary install at the workspace root  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/291 [(90918)](https://github.com/alchemy-run/alchemy-effect/commit/9091856e) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.35...HEAD) + +--- + +## v2.0.0-beta.35 + +###    🚀 Features + +- **Cloudflare**: + - **R2**: Add bucket custom domains  -  by **Alex** and **sam** in https://github.com/alchemy-run/alchemy-effect/issues/241 [(a782a)](https://github.com/alchemy-run/alchemy-effect/commit/a782a449) +- **Neon**: + - **Project**: Add enableLogicalReplication option  -  by **Baptiste Arnaud** in https://github.com/alchemy-run/alchemy-effect/issues/268 [(c4945)](https://github.com/alchemy-run/alchemy-effect/commit/c49451c6) +- **cli**: + - Warn when a newer alchemy version is on npm  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/253 [(7e701)](https://github.com/alchemy-run/alchemy-effect/commit/7e701205) + - Use plain-text renderer in non-interactive terminals  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/258 [(16485)](https://github.com/alchemy-run/alchemy-effect/commit/16485403) + - Cloudflare/aws namespaces + cloudflare state logs  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/256 [(335ab)](https://github.com/alchemy-run/alchemy-effect/commit/335ab26c) +- **cloudflare**: + - Worker support non-string env bindings  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/269 [(220c3)](https://github.com/alchemy-run/alchemy-effect/commit/220c358c) + - **queue**: Cloudflare.messages(queue).subscribe(...) Effect consumer API  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/260 [(59c01)](https://github.com/alchemy-run/alchemy-effect/commit/59c01867) + +###    🐞 Bug Fixes + +- Move Auth Providers into layers to keep requirements never  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/271 [(38319)](https://github.com/alchemy-run/alchemy-effect/commit/38319fa0) +- Dev mode node compatibility and profiles  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/273 [(9f402)](https://github.com/alchemy-run/alchemy-effect/commit/9f4026b6) +- **Cloudflare**: + - **QueueConsumer**: Paginate listConsumers, surface conflicts clearly  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/257 [(11d06)](https://github.com/alchemy-run/alchemy-effect/commit/11d06201) + - **R2**: Simplify r2 custom domain interface to just an array  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/259 [(8953d)](https://github.com/alchemy-run/alchemy-effect/commit/8953dfc2) +- **Drizzle**: + - Only flag Schema as updated when migrations actually drift  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/261 [(8f2e3)](https://github.com/alchemy-run/alchemy-effect/commit/8f2e33e7) +- **cloudflare**: + - Disable builder.sharedConfigBuild for vite build  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/176 [(2fb34)](https://github.com/alchemy-run/alchemy-effect/commit/2fb34c2b) + - Update runtime to 0.3.1  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/278 [(426e8)](https://github.com/alchemy-run/alchemy-effect/commit/426e83f6) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.34...HEAD) + +--- + +## v2.0.0-beta.34 + +###    🐞 Bug Fixes + +- Upgrade to distilled 0.17.0 consistently  -  by **Sam Goodwin** [(d681f)](https://github.com/alchemy-run/alchemy-effect/commit/d681f72f) +- **cli**: Correct args for dev command in node  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/250 [(4859d)](https://github.com/alchemy-run/alchemy-effect/commit/4859dc95) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.33...HEAD) + +--- + +## v2.0.0-beta.33 + +###    🐞 Bug Fixes + +- **Cloudflare**: Fix I/O error in state store  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/251 [(ec430)](https://github.com/alchemy-run/alchemy-effect/commit/ec430ad2) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.32...HEAD) + +--- + +## v2.0.0-beta.32 + +###    🚀 Features + +- **cloudflare**: Log cf state store version mismatch  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/245 [(75f52)](https://github.com/alchemy-run/alchemy-effect/commit/75f5220d) + +###    🐞 Bug Fixes + +- **cloudflare**: Remove debug log statements  -  by **Sam Goodwin** [(64414)](https://github.com/alchemy-run/alchemy-effect/commit/64414547) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.31...HEAD) + +--- + +## v2.0.0-beta.31 + +###    🚀 Features + +- **Cloudflare**: + - **StateStore**: Wire OTLP traces, metrics, and logs  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/181 [(14b8a)](https://github.com/alchemy-run/alchemy-effect/commit/14b8ab8c) +- **aws**: + - Harden ApiGateway REST resources (bindings, retries, stage patches)  -  by **Sam Goodwin** and **Adrian Witaszak** in https://github.com/alchemy-run/alchemy-effect/issues/169 [(e4b84)](https://github.com/alchemy-run/alchemy-effect/commit/e4b84a51) +- **cloudflare**: + - Add Tunnel, VpcService, and VpcServiceRef  -  by **Kotkoroid** and **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/123 [(090c1)](https://github.com/alchemy-run/alchemy-effect/commit/090c14db) + - Expose raw durable object sql storage  -  by **m@s** in https://github.com/alchemy-run/alchemy-effect/issues/188 [(20c03)](https://github.com/alchemy-run/alchemy-effect/commit/20c0301b) + - Add Hyperdrive resource with dev-mode local DB support  -  by **Julian Archila** in https://github.com/alchemy-run/alchemy-effect/issues/158 [(8e651)](https://github.com/alchemy-run/alchemy-effect/commit/8e651091) + - Workers observability traces  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/244 [(ae531)](https://github.com/alchemy-run/alchemy-effect/commit/ae5319cd) +- **core**: + - Support for adoption in the core engine (using read)  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/167 [(9914e)](https://github.com/alchemy-run/alchemy-effect/commit/9914ec03) + - Replace create+update with one `reconcile` function  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/179 [(bfd4f)](https://github.com/alchemy-run/alchemy-effect/commit/bfd4f386) +- **neon**: + - Add Neon serverless Postgres provider  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/134 [(3b5e8)](https://github.com/alchemy-run/alchemy-effect/commit/3b5e88a8) +- **test**: + - Add dev flag to test harness, toggleable via ALCHEMY_DEV  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/183 [(7856a)](https://github.com/alchemy-run/alchemy-effect/commit/7856afbb) +- **website**: + - Generate llms.txt from page frontmatter  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/170 [(9ad47)](https://github.com/alchemy-run/alchemy-effect/commit/9ad47381) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Dedupe Container DO bindings by namespaceId  -  by **Christopher Yovanovitch** and **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/172 [(a2c54)](https://github.com/alchemy-run/alchemy-effect/commit/a2c54999) + - **SecretsStore**: Adoption opt-in by default  -  by **Stibbs** in https://github.com/alchemy-run/alchemy-effect/issues/163 [(d9f22)](https://github.com/alchemy-run/alchemy-effect/commit/d9f22752) +- **aws**: + - **lambda**: + - Scope public url invokes  -  by **José Netto** in https://github.com/alchemy-run/alchemy-effect/issues/162 [(1e092)](https://github.com/alchemy-run/alchemy-effect/commit/1e092912) + - Pass invoked via url permission  -  by **José Netto** in https://github.com/alchemy-run/alchemy-effect/issues/161 [(3e978)](https://github.com/alchemy-run/alchemy-effect/commit/3e978ff7) +- **cloudflare**: + - Enable hyperdrive in dev mode  -  by **John Royal** and **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/242 [(86623)](https://github.com/alchemy-run/alchemy-effect/commit/8662373d) + - **worker**: Make asset hash path-independent and reuse manifest when unchanged  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/165 [(e1669)](https://github.com/alchemy-run/alchemy-effect/commit/e1669a77) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.30...HEAD) + +--- + +## v2.0.0-beta.30 + +###    🐞 Bug Fixes + +- **Cloudflare**: Apply Cloudflare Access headers to HTTP requests  -  by **jacobiajohnson** and **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/160 [(fd329)](https://github.com/alchemy-run/alchemy-effect/commit/fd329e7) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.29...HEAD) + +--- + +## v2.0.0-beta.29 + +###    🚀 Features + +- **Cloudflare**: Add AiGateway resource  -  by **Jan Henning** in https://github.com/alchemy-run/alchemy-effect/issues/130 [(b0361)](https://github.com/alchemy-run/alchemy-effect/commit/b036165) +- **cloudflare/state-store**: Version-gate the deployed worker  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/156 [(bdc37)](https://github.com/alchemy-run/alchemy-effect/commit/bdc37df) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Don't run update out of order for strict DAGs  -  by **sam** and **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/144 [(fcae5)](https://github.com/alchemy-run/alchemy-effect/commit/fcae571) + - Run state-store secret probe under the deployed script name  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/150 [(061f8)](https://github.com/alchemy-run/alchemy-effect/commit/061f86b) +- **cloudflare**: + - Handle cloudflare access for edge preview workers  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/155 [(09d01)](https://github.com/alchemy-run/alchemy-effect/commit/09d0130) +- **core**: + - Handle failed resource gracefully  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/119 [(94d4b)](https://github.com/alchemy-run/alchemy-effect/commit/94d4b3f) +- **website**: + - Mobile dark hero, theme reactivity, brighter beta badge, OG host  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/146 [(05eae)](https://github.com/alchemy-run/alchemy-effect/commit/05eae4d) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.28...HEAD) + +--- + +## v2.0.0-beta.28 + +###    🐞 Bug Fixes + +- Remove PlatformServices from Util barrel  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/137 [(f85dd)](https://github.com/alchemy-run/alchemy-effect/commit/f85ddfe) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.27...HEAD) + +--- + +## v2.0.0-beta.27 + +###    🐞 Bug Fixes + +- **Cloudflare**: Don't bundle CLI with tsdown because it messes with import.meta.path  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/135 [(2796c)](https://github.com/alchemy-run/alchemy-effect/commit/2796c33) +- **pr-package**: Use `bun add @` to dodge bun DependencyLoop bug  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/132 [(989e2)](https://github.com/alchemy-run/alchemy-effect/commit/989e254) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.26...HEAD) + +--- + +## v2.0.0-beta.26 + +###    🚀 Features + +- **test**: Align vitest and bun test helpers and integrate with profiles  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/129 [(07e83)](https://github.com/alchemy-run/alchemy-effect/commit/07e83f3) + +###    🐞 Bug Fixes + +- Export types as lib/.d.ts  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/121 [(a000c)](https://github.com/alchemy-run/alchemy-effect/commit/a000c26) +- Resolve bin/exec.ts via package name so devs modes bundled CLI works  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/128 [(303da)](https://github.com/alchemy-run/alchemy-effect/commit/303daeb) +- **cli**: + - Parallelize alchemy state tree  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/126 [(f3e8f)](https://github.com/alchemy-run/alchemy-effect/commit/f3e8fef) +- **cloudflare**: + - Bootstrap regressions in SecretsStore and StateStore  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/131 [(37db0)](https://github.com/alchemy-run/alchemy-effect/commit/37db01a) +- **core**: + - Fix broken url in error message and add state store guide  -  by **sam** [(4b991)](https://github.com/alchemy-run/alchemy-effect/commit/4b991e7) + - Treat raw Resource refs as upstream dependencies  -  by **Mathieu Post** and **sam** in https://github.com/alchemy-run/alchemy-effect/issues/122 [(416f1)](https://github.com/alchemy-run/alchemy-effect/commit/416f19c) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.25...HEAD) + +--- + +## v2.0.0-beta.25 + +###    🐞 Bug Fixes + +- **Cloudflare**: Remove broken keepAssets optimization  -  by **Sam Goodwin** [(845a7)](https://github.com/alchemy-run/alchemy-effect/commit/845a7b7) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.24...HEAD) + +--- + +## v2.0.0-beta.24 + +###    🚀 Features + +- Dev mode for cloudflare workers  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/88 [(5a0d7)](https://github.com/alchemy-run/alchemy-effect/commit/5a0d779) +- **core**: --adopt  -  by **Sam Goodwin** [(2af88)](https://github.com/alchemy-run/alchemy-effect/commit/2af8851) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Add bootstrap command and harden bootstrap process of cloudflare  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/118 [(0858d)](https://github.com/alchemy-run/alchemy-effect/commit/0858dd8) + - Hoist stack non-destructively  -  by **Sam Goodwin** [(c9621)](https://github.com/alchemy-run/alchemy-effect/commit/c9621d1) + - Don't fail on broken paths in state  -  by **Sam Goodwin** [(05f16)](https://github.com/alchemy-run/alchemy-effect/commit/05f16c8) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.23...HEAD) + +--- + +## v2.0.0-beta.23 + +###    🐞 Bug Fixes + +- **AWS**: Various bugs in Lambda Function  -  by **Sam Goodwin** [(92611)](https://github.com/alchemy-run/alchemy-effect/commit/92611cd) +- **cli**: Logs and tail command read state from right place  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/114 [(00163)](https://github.com/alchemy-run/alchemy-effect/commit/0016361) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.22...HEAD) + +--- + +## v2.0.0-beta.22 + +###    🚀 Features + +- **Cloudflare**: D1Database migrationsDir, importFiles, and clone  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/111 [(555e8)](https://github.com/alchemy-run/alchemy-effect/commit/555e8ff) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.21...HEAD) + +--- + +## v2.0.0-beta.21 + +###    🚀 Features + +- **pr-pkg**: Pr-package package  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/110 [(9442d)](https://github.com/alchemy-run/alchemy-effect/commit/9442d42) + +###    🐞 Bug Fixes + +- Update to distilled 0.13.1  -  by **Sam Goodwin** [(b0407)](https://github.com/alchemy-run/alchemy-effect/commit/b040783) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.20...HEAD) + +--- + +## v2.0.0-beta.20 + +###    🐞 Bug Fixes + +- Update to effect beta.58 & distilled 0.13  -  by **Michael (Pear)** [(d6f4e)](https://github.com/alchemy-run/alchemy-effect/commit/d6f4e53) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.19...HEAD) + +--- + +## v2.0.0-beta.19 + +###    🚀 Features + +- **Axiom**: + - Axiom resources  -  by **Sam Goodwin** [(f2ffd)](https://github.com/alchemy-run/alchemy-effect/commit/f2ffde1) + - Type-safe dashboard charts  -  by **Sam Goodwin** [(46f05)](https://github.com/alchemy-run/alchemy-effect/commit/46f051d) +- **Bundle**: + - Auto-detect entry package for pure annotations  -  by **Sam Goodwin** [(ce5f3)](https://github.com/alchemy-run/alchemy-effect/commit/ce5f35b) +- **core**: + - Annotate pure calls to optimize tree shaking  -  by **Sam Goodwin** [(e21a6)](https://github.com/alchemy-run/alchemy-effect/commit/e21a605) + +###    🐞 Bug Fixes + +- **Axiom**: + - Ensure token is always present and handle missing token error in ApiTokenProvider  -  by **Sam Goodwin** [(da86a)](https://github.com/alchemy-run/alchemy-effect/commit/da86abb) + - Don't replace token when props don't cange  -  by **Sam Goodwin** [(ac655)](https://github.com/alchemy-run/alchemy-effect/commit/ac65581) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.18...v2.0.0-beta.19) + +--- + +## v2.0.0-beta.18 + +###    🚀 Features + +- **CloudFront**: CachePolicy, OriginRequestPolicy, ResponseHeadersPolicy, PublicKey, KeyGroup  -  by **Jordan** and **Claude Opus 4.7** in https://github.com/alchemy-run/alchemy-effect/issues/106 [(70201)](https://github.com/alchemy-run/alchemy-effect/commit/7020108) +- **GitHub**: Secrets helper for creating many secrets  -  by **Sam Goodwin** [(7be53)](https://github.com/alchemy-run/alchemy-effect/commit/7be536a) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.17...v2.0.0-beta.18) + +--- + +## v2.0.0-beta.17 + +###    🚀 Features + +- **ECS**: + - Add CapacityProvider resource  -  by **Jordan** in https://github.com/alchemy-run/alchemy-effect/issues/103 [(cb218)](https://github.com/alchemy-run/alchemy-effect/commit/cb2183e) +- **cli**: + - Add alchemy profile clear command  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/104 [(9dde9)](https://github.com/alchemy-run/alchemy-effect/commit/9dde996) + - Add `alchemy state clear` command  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/105 [(26bf5)](https://github.com/alchemy-run/alchemy-effect/commit/26bf5e4) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Don't modify ApiToken policies, let user write explicitly.  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/102 [(5c31b)](https://github.com/alchemy-run/alchemy-effect/commit/5c31b1f) +- **GitHub**: + - Place Comment in the GitHub.Providers collection  -  by **Sam Goodwin** [(3ad95)](https://github.com/alchemy-run/alchemy-effect/commit/3ad95cb) +- **cli**: + - Don't prompt for approval if plan has no changes  -  by **Sam Goodwin** [(891cf)](https://github.com/alchemy-run/alchemy-effect/commit/891cf1c) + - Dont print undefined outputs  -  by **Sam Goodwin** [(31ade)](https://github.com/alchemy-run/alchemy-effect/commit/31ade04) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.16...v2.0.0-beta.17) + +--- + +## v2.0.0-beta.16 + +###    🚀 Features + +- **Cloudflare**: + - Artifacts Store  -  by **Sam Goodwin** [(e531f)](https://github.com/alchemy-run/alchemy-effect/commit/e531fbe) + - Allow account ID to be passed into AccountApiToken and UserApiToken  -  by **Sam Goodwin** [(0f8ac)](https://github.com/alchemy-run/alchemy-effect/commit/0f8ac88) +- **GitHub**: + - Auth provider for github supporting env, PAT and gh cli  -  by **Sam Goodwin** [(1de51)](https://github.com/alchemy-run/alchemy-effect/commit/1de5180) +- **cli**: + - Alchemy profile show  -  by **Sam Goodwin** [(f83e2)](https://github.com/alchemy-run/alchemy-effect/commit/f83e216) + +###    🐞 Bug Fixes + +- **Cloudflare**: Remove InstanceId from Artifact store requirement  -  by **Sam Goodwin** [(de489)](https://github.com/alchemy-run/alchemy-effect/commit/de489c7) +- **cli**: Don't fail on broken profiles when logging in  -  by **Sam Goodwin** [(d8296)](https://github.com/alchemy-run/alchemy-effect/commit/d829616) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.15...v2.0.0-beta.16) + +--- + +## v2.0.0-beta.15 + +###    🚀 Features + +- **Cloudflare**: UserApiToken and AccountApiToken  -  by **Sam Goodwin** [(56e0f)](https://github.com/alchemy-run/alchemy-effect/commit/56e0f79) +- **GitHub**: Export a providers() Layer  -  by **Sam Goodwin** [(e5cd4)](https://github.com/alchemy-run/alchemy-effect/commit/e5cd471) + +###    🐞 Bug Fixes + +- Properly handle redacted outputs  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/101 [(0d049)](https://github.com/alchemy-run/alchemy-effect/commit/0d049aa) +- **Cloudflare**: + - Prefix State Store secrets with Alchemy  -  by **Sam Goodwin** [(e2533)](https://github.com/alchemy-run/alchemy-effect/commit/e25338e) +- **core**: + - Clearer error when Stack is missing state or providers  -  by **Michael K** [(52cf4)](https://github.com/alchemy-run/alchemy-effect/commit/52cf431) + - Core no longer requires cli  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/100 [(aba0e)](https://github.com/alchemy-run/alchemy-effect/commit/aba0ea4) + - Handle Redacted values in Plan  -  by **Sam Goodwin** [(4fc48)](https://github.com/alchemy-run/alchemy-effect/commit/4fc4874) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.14...v2.0.0-beta.15) + +--- + +## v2.0.0-beta.14 + +###    🚀 Features + +- **Cloudflare**: Cloudflare State Store  -  by **Sam Goodwin** and **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/94 [(d3b12)](https://github.com/alchemy-run/alchemy-effect/commit/d3b1298) +- **cli**: Alchemy state command  -  by **Sam Goodwin** [(b4a1d)](https://github.com/alchemy-run/alchemy-effect/commit/b4a1d94) + +###    🐞 Bug Fixes + +- **Cloudflare**: Set rolldown resolve.conditionNames for bun/node in container bundle  -  by **Christopher Yovanovitch** in https://github.com/alchemy-run/alchemy-effect/issues/86 [(779bb)](https://github.com/alchemy-run/alchemy-effect/commit/779bbd1) +- **cli**: Use a cli.js shim to support node/bun and make effect a non-optional peer  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/92 [(9b69b)](https://github.com/alchemy-run/alchemy-effect/commit/9b69ba3) +- **core**: Harden HttpStateStore to transient errors  -  by **Sam Goodwin** [(4071e)](https://github.com/alchemy-run/alchemy-effect/commit/4071e8a) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.13...v2.0.0-beta.14) + +--- + +## v2.0.0-beta.13 + +###    🐞 Bug Fixes + +- **cli**: Cli ships correct platform versions  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/91 [(e3597)](https://github.com/alchemy-run/alchemy-effect/commit/e3597ab) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.12...v2.0.0-beta.13) + +--- + +## v2.0.0-beta.12 + +###    🚀 Features + +- **AWS**: Use account-regional S3 bucket names for assets  -  by **Sam Goodwin** [(6ab3f)](https://github.com/alchemy-run/alchemy-effect/commit/6ab3fba) +- **cloudflare**: Secret store resources  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/64 [(9b107)](https://github.com/alchemy-run/alchemy-effect/commit/9b10769) + +###    🐞 Bug Fixes + +- Providers not inheriting parent scope  -  by **John Royal** [(8317b)](https://github.com/alchemy-run/alchemy-effect/commit/8317bcc) +- Include WebSocketConstructor in PlatformServices  -  by **John Royal** [(2dfb0)](https://github.com/alchemy-run/alchemy-effect/commit/2dfb06e) + +###    🏎 Performance + +- Use subpath imports for PlatformServices  -  by **John Royal** [(1867a)](https://github.com/alchemy-run/alchemy-effect/commit/1867a42) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.11...v2.0.0-beta.12) + +--- + +## v2.0.0-beta.11 + +###    🚀 Features + +- Support vite 7  -  by **John Royal** and **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/68 [(28d0e)](https://github.com/alchemy-run/alchemy-effect/commit/28d0ef2) +- **Cloudflare**: + - Install external deps in container Dockerfile  -  by **Christopher Yovanovitch** and **Sisyphus** in https://github.com/alchemy-run/alchemy-effect/issues/75 [(f8493)](https://github.com/alchemy-run/alchemy-effect/commit/f84934f) + - Add Queue, QueueBinding, and QueueConsumer resources  -  by **Christopher Yovanovitch** and **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/69 [(09e97)](https://github.com/alchemy-run/alchemy-effect/commit/09e971b) +- **cli**: + - Alchemy login  -  by **Sam Goodwin** and **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/67 [(fc57d)](https://github.com/alchemy-run/alchemy-effect/commit/fc57db8) + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Provide WorkerEnvironment to Workflow body, not wrapper  -  by **Christopher Yovanovitch** in https://github.com/alchemy-run/alchemy-effect/issues/71 [(a0ae1)](https://github.com/alchemy-run/alchemy-effect/commit/a0ae1d6) + - Write all bundle chunks to container image context  -  by **Christopher Yovanovitch** in https://github.com/alchemy-run/alchemy-effect/issues/78 [(03306)](https://github.com/alchemy-run/alchemy-effect/commit/0330605) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.10...v2.0.0-beta.11) + +--- + +## v2.0.0-beta.10 + +###    🐞 Bug Fixes + +- **Cloudflare**: + - Reduce readiness retry schedule from 104s to 30s  -  by **Christopher Yovanovitch** and **Sisyphus** in https://github.com/alchemy-run/alchemy-effect/issues/76 [(bb2bf)](https://github.com/alchemy-run/alchemy-effect/commit/bb2bff2) + - Await monitor() Promise instead of discarding it  -  by **Christopher Yovanovitch** and **Sisyphus** in https://github.com/alchemy-run/alchemy-effect/issues/74 [(d75e6)](https://github.com/alchemy-run/alchemy-effect/commit/d75e6dd) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.9...v2.0.0-beta.10) + +--- + +## v2.0.0-beta.9 + +###    🚀 Features + +- CLI packages  -  by **Michael K** in https://github.com/alchemy-run/alchemy-effect/issues/66 [(73e2a)](https://github.com/alchemy-run/alchemy-effect/commit/73e2a45) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.8...v2.0.0-beta.9) + +--- + +## v2.0.0-beta.8 + +###    🐞 Bug Fixes + +- Add stack export to package.json  -  by **John Royal** [(0ede2)](https://github.com/alchemy-run/alchemy-effect/commit/0ede2c3) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.7...v2.0.0-beta.8) + +--- + +## v2.0.0-beta.7 + +###    🐞 Bug Fixes + +- **cloudflare**: Use consistent comaptibility date and flags in Worker  -  by **Sam Goodwin** [(deda3)](https://github.com/alchemy-run/alchemy-effect/commit/deda381) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.6...v2.0.0-beta.7) + +--- + +## v2.0.0-beta.6 + +###    🐞 Bug Fixes + +- **cloudflare**: Set nodejs_compat by default for Effect Workers  -  by **Sam Goodwin** [(ff780)](https://github.com/alchemy-run/alchemy-effect/commit/ff780f3) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.5...v2.0.0-beta.6) + +--- + +## v2.0.0-beta.5 + +###    🐞 Bug Fixes + +- Running with bun no longer runs unbundled alchemy bin  -  by **Michael (Pear)** [(61198)](https://github.com/alchemy-run/alchemy-effect/commit/61198ca) +- **core**: Use Provider collection as requirements  -  by **Sam Goodwin** [(f81cc)](https://github.com/alchemy-run/alchemy-effect/commit/f81cccb) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.4...v2.0.0-beta.5) + +--- + +## v2.0.0-beta.4 + +###    🚀 Features + +- Pr-package service  -  by **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/54 [(6edd8)](https://github.com/alchemy-run/alchemy-effect/commit/6edd8dd) +- **cloudflare**: + - Async DurableObjetNamespace binding  -  by **Sam Goodwin** [(25b90)](https://github.com/alchemy-run/alchemy-effect/commit/25b9012) + - Async Assets binding  -  by **Sam Goodwin** [(f1a3c)](https://github.com/alchemy-run/alchemy-effect/commit/f1a3c6f) + - Alarms and ScheduledEvents  -  by **Sam Goodwin** [(1f183)](https://github.com/alchemy-run/alchemy-effect/commit/1f183c0) + - Worker Custom Domains  -  by **Sam Goodwin** [(670c5)](https://github.com/alchemy-run/alchemy-effect/commit/670c575) +- **core**: + - AuthProvider  -  by **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/61 [(bac01)](https://github.com/alchemy-run/alchemy-effect/commit/bac01b7) + +###    🐞 Bug Fixes + +- Rename cli binary from "alchemy-effect" to "alchemy"  -  by **John Royal** [(b8b3a)](https://github.com/alchemy-run/alchemy-effect/commit/b8b3a8c) +- Fix exports  -  by **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/62 [(903c2)](https://github.com/alchemy-run/alchemy-effect/commit/903c262) +- **aws**: Fix VpcEndpoint resource bad state checks  -  by **Michael (Pear)** and **Marc MacLeod** in https://github.com/alchemy-run/alchemy-effect/issues/63 [(f1fd2)](https://github.com/alchemy-run/alchemy-effect/commit/f1fd20b) +- **core**: Use platform-bun when running in bun, otherwise platform-node  -  by **Sam Goodwin** [(3b0d6)](https://github.com/alchemy-run/alchemy-effect/commit/3b0d6b0) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.3...v2.0.0-beta.4) + +--- + +## v2.0.0-beta.test-export-fix-4 + +_No significant changes_ + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.test-export-fix-3...v2.0.0-beta.test-export-fix-4) + +--- + +## v2.0.0-beta.test-export-fix-3 + +_No significant changes_ + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.test-export-fix-2...v2.0.0-beta.test-export-fix-3) + +--- + +## v2.0.0-beta.test-export-fix-2 + +_No significant changes_ + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.test-export-fix...v2.0.0-beta.test-export-fix-2) + +--- + +## v2.0.0-beta.test-export-fix + +###    🚀 Features + +- Pr-package service  -  by **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/54 [(6edd8)](https://github.com/alchemy-run/alchemy-effect/commit/6edd8dd) +- **cloudflare**: + - Async DurableObjetNamespace binding  -  by **Sam Goodwin** [(25b90)](https://github.com/alchemy-run/alchemy-effect/commit/25b9012) + - Async Assets binding  -  by **Sam Goodwin** [(f1a3c)](https://github.com/alchemy-run/alchemy-effect/commit/f1a3c6f) + - Alarms and ScheduledEvents  -  by **Sam Goodwin** [(1f183)](https://github.com/alchemy-run/alchemy-effect/commit/1f183c0) + +###    🐞 Bug Fixes + +- Rename cli binary from "alchemy-effect" to "alchemy"  -  by **John Royal** [(b8b3a)](https://github.com/alchemy-run/alchemy-effect/commit/b8b3a8c) +- Fix export order  -  by **Michael (Pear)** [(7bea9)](https://github.com/alchemy-run/alchemy-effect/commit/7bea945) +- **core**: Use platform-bun when running in bun, otherwise platform-node  -  by **Sam Goodwin** [(3b0d6)](https://github.com/alchemy-run/alchemy-effect/commit/3b0d6b0) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.3...v2.0.0-beta.test-export-fix) + +--- + +## v2.0.0-beta.3 + +###    🚀 Features + +- **GitHub**: + - Comment Resource and CI guide  -  by **Sam Goodwin** [(5e938)](https://github.com/alchemy-run/alchemy-effect/commit/5e93857) + - Secret and Variable  -  by **Sam Goodwin** [(0f620)](https://github.com/alchemy-run/alchemy-effect/commit/0f620bd) + +###    🐞 Bug Fixes + +- **core**: Fix undefined Provider in plan  -  by **Sam Goodwin** [(e0143)](https://github.com/alchemy-run/alchemy-effect/commit/e01431f) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.2...v2.0.0-beta.3) + +--- + +## v2.0.0-beta.2 + +###    🚀 Features + +- ProviderCollections reduce number of Provider types polluting user's types  -  by **Sam Goodwin** [(9f76b)](https://github.com/alchemy-run/alchemy-effect/commit/9f76b91) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v2.0.0-beta.1...v2.0.0-beta.2) + +--- + +## v2.0.0-beta.1 + +###    🚀 Features + +- Migrate to effect@4.0.0-beta.48  -  by **Sam Goodwin** [(8674e)](https://github.com/alchemy-run/alchemy-effect/commit/8674e8f) +- **cloudflare**: Support R2Bucket.bind instead of R2BucketBinding.bind for all CF resources  -  by **Sam Goodwin** [(12b50)](https://github.com/alchemy-run/alchemy-effect/commit/12b50d3) +- **core**: Alchemy.Stack  -  by **Sam Goodwin** [(68e66)](https://github.com/alchemy-run/alchemy-effect/commit/68e6622) + +###    🐞 Bug Fixes + +- Release to alchemy package  -  by **Michael (Pear)** in https://github.com/alchemy-run/alchemy-effect/issues/55 [(217c5)](https://github.com/alchemy-run/alchemy-effect/commit/217c5c9) +- **cloudflare**: + - Require Content Length in R2.put and use raw ReadableStream if available  -  by **Sam Goodwin** [(1cd6b)](https://github.com/alchemy-run/alchemy-effect/commit/1cd6bda) + - Bundle properly formats windows paths  -  by **Michael (Pear)** [(5156c)](https://github.com/alchemy-run/alchemy-effect/commit/5156c6c) +- **test**: + - Infer and resolve Output types in Test/Bun  -  by **Sam Goodwin** [(9853d)](https://github.com/alchemy-run/alchemy-effect/commit/9853d92) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.11.0...v2.0.0-beta.1) + +--- + +## v0.11.0 + +###    🚀 Features + +- **test**: Add skipIf to bun testing harness  -  by **Michael (Pear)** [(6021c)](https://github.com/alchemy-run/alchemy-effect/commit/6021c3d) + +###    🐞 Bug Fixes + +- **Http**: Lazy-import platform-node to avoid crash on Bun  -  by **Christopher Yovanovitch** and **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/48 [(85224)](https://github.com/alchemy-run/alchemy-effect/commit/852243b) +- **cloudflare**: Bindings resolve Effect and remove KV per-operation bindings  -  by **Sam Goodwin** [(61e02)](https://github.com/alchemy-run/alchemy-effect/commit/61e0244) +- **core**: Migrate to Provider.effect so that providers are tree-shakable  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/53 [(73029)](https://github.com/alchemy-run/alchemy-effect/commit/7302911) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.10.0...v0.11.0) + +--- + +## v0.10.0 + +###    🚀 Features + +- **cloudflare**: + - Accept Output in Worker env + Vite props  -  by **cooper** in https://github.com/alchemy-run/alchemy-effect/issues/51 [(9560b)](https://github.com/alchemy-run/alchemy-effect/commit/9560bed) + - Allow bindings to be undefined  -  by **Sam Goodwin** [(15c8c)](https://github.com/alchemy-run/alchemy-effect/commit/15c8c6e) + - Rename DynamicWorker to DynamicWorkerLoader  -  by **Sam Goodwin** [(39f89)](https://github.com/alchemy-run/alchemy-effect/commit/39f89d5) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.9.1...v0.10.0) + +--- + +## v0.9.1 + +###    🐞 Bug Fixes + +- **cloudflare**: Environment variables silently dropped  -  by **cooper** in https://github.com/alchemy-run/alchemy-effect/issues/49 [(847f0)](https://github.com/alchemy-run/alchemy-effect/commit/847f0c2) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.9.0...v0.9.1) + +--- + +## v0.9.0 + +###    🚀 Features + +- **core**: Bun test helpers  -  by **Sam Goodwin** [(40af3)](https://github.com/alchemy-run/alchemy-effect/commit/40af3f4) + +###    🐞 Bug Fixes + +- **cloudflare**: + - Delay WorkerEnvironment resolution for R2Bucket binding until inside Worker  -  by **Sam Goodwin** [(b41b1)](https://github.com/alchemy-run/alchemy-effect/commit/b41b1c3) + - Precreate Worker with tags and resolve DO namespace IDs as Worker output attributes  -  by **Sam Goodwin** [(c6b2a)](https://github.com/alchemy-run/alchemy-effect/commit/c6b2add) + - Retry eventually consistent ContainerApplicationNotFound error  -  by **Sam Goodwin** [(90d75)](https://github.com/alchemy-run/alchemy-effect/commit/90d7524) + - Resolve Durable Object namespace IDs in precreate  -  by **Sam Goodwin** [(cd1e2)](https://github.com/alchemy-run/alchemy-effect/commit/cd1e25d) + - Use Sonda to generate bundle report  -  by **Sam Goodwin** [(a7a9c)](https://github.com/alchemy-run/alchemy-effect/commit/a7a9c5f) + - Recover from partial ContainerApplication failure  -  by **Sam Goodwin** [(04890)](https://github.com/alchemy-run/alchemy-effect/commit/04890d3) +- **core**: + - Use a Deferred to support parallel caching  -  by **Sam Goodwin** [(ca693)](https://github.com/alchemy-run/alchemy-effect/commit/ca693b5) + - Exclude Artifacts from provider requirements  -  by **Sam Goodwin** [(a0aec)](https://github.com/alchemy-run/alchemy-effect/commit/a0aec34) + - Resolve Effects in Binding.Sevice.bind  -  by **Sam Goodwin** [(b82f2)](https://github.com/alchemy-run/alchemy-effect/commit/b82f262) +- **test**: + - Return output of deploying stack in a test  -  by **Sam Goodwin** [(6d354)](https://github.com/alchemy-run/alchemy-effect/commit/6d354a1) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.8.0...v0.9.0) + +--- + +## v0.8.0 + +###    🚀 Features + +- **cloudflare**: Async-alchemy style bindings and R2 wrapper  -  by **Sam Goodwin** [(4c4ca)](https://github.com/alchemy-run/alchemy-effect/commit/4c4ca91) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.7.1...v0.8.0) + +--- + +## v0.7.1 + +###    🚀 Features + +- **core**: Support deploy --force  -  by **sam** in https://github.com/alchemy-run/alchemy-effect/issues/43 [(06b88)](https://github.com/alchemy-run/alchemy-effect/commit/06b881a) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.7.0...v0.7.1) + +--- + +## v0.7.0 + +###    🚀 Features + +- **cloudflare**: + - Vite integration  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/41 [(e3c16)](https://github.com/alchemy-run/alchemy-effect/commit/e3c160c) +- **core**: + - Add Output.as()  -  by **Sam Goodwin** [(33186)](https://github.com/alchemy-run/alchemy-effect/commit/33186ef) + - Artifact service for caching build artifacts across plan and apply  -  by **Sam Goodwin** [(dc60e)](https://github.com/alchemy-run/alchemy-effect/commit/dc60e9e) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.6.4...v0.7.0) + +--- + +## v0.6.4 + +_No significant changes_ + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.6.3...v0.6.4) + +--- + +## v0.6.3 + +###    🚀 Features + +- **better-auth**: + - Add @alchemy.run/better-auth packag with D1 support  -  by **Sam Goodwin** [(9dc0f)](https://github.com/alchemy-run/alchemy-effect/commit/9dc0ff6) +- **cloudflare**: + - D1Database resource  -  by **Sam Goodwin** [(48fd4)](https://github.com/alchemy-run/alchemy-effect/commit/48fd499) + - Expose raw Cloudflare.Request as a Tag  -  by **Sam Goodwin** [(67998)](https://github.com/alchemy-run/alchemy-effect/commit/67998cd) +- **core**: + - Random resource  -  by **Sam Goodwin** [(71527)](https://github.com/alchemy-run/alchemy-effect/commit/715278e) + +###    🐞 Bug Fixes + +- Replace Schedule.compose with Schedule.both  -  by **Sam Goodwin** [(46195)](https://github.com/alchemy-run/alchemy-effect/commit/461951d) +- **cli**: + - Simplify logs query to latest hour  -  by **Sam Goodwin** [(b4a47)](https://github.com/alchemy-run/alchemy-effect/commit/b4a4733) + - Used TaggedError in Daemon  -  by **Sam Goodwin** [(77a78)](https://github.com/alchemy-run/alchemy-effect/commit/77a788a) +- **cloudflare**: + - Export D1 resources and bindings  -  by **Sam Goodwin** [(d9e94)](https://github.com/alchemy-run/alchemy-effect/commit/d9e9447) + - Export D1 resources and bindings  -  by **Sam Goodwin** [(bcd43)](https://github.com/alchemy-run/alchemy-effect/commit/bcd436d) +- **core**: + - Support updating downstream to unlink and delete a binding  -  by **Sam Goodwin** [(6bdf8)](https://github.com/alchemy-run/alchemy-effect/commit/6bdf8e9) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.6.2...v0.6.3) + +--- + +## v0.6.2 + +_No significant changes_ + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.6.1...v0.6.2) + +--- + +## v0.6.1 + +###    🚀 Features + +- Move to distilled-aws  -  by **Sam Goodwin** [(e12f2)](https://github.com/alchemy-run/alchemy-effect/commit/e12f2a6) +- Add S3 data plane APIs  -  by **Sam Goodwin** and **Claude Haiku 4.5** in https://github.com/alchemy-run/alchemy-effect/issues/34 [(f2986)](https://github.com/alchemy-run/alchemy-effect/commit/f2986ab) +- Bootstrap CLI command  -  by **Sam Goodwin** [(3a79a)](https://github.com/alchemy-run/alchemy-effect/commit/3a79a04) +- Lambda.consumeTable  -  by **Sam Goodwin** [(420c1)](https://github.com/alchemy-run/alchemy-effect/commit/420c124) +- Remove ExecutionContext from Policy requirements  -  by **Sam Goodwin** [(867ef)](https://github.com/alchemy-run/alchemy-effect/commit/867ef28) +- Implement HttpServer for Lambda and Cloudflare  -  by **Sam Goodwin** [(655df)](https://github.com/alchemy-run/alchemy-effect/commit/655dfd1) +- Namespaces  -  by **Sam Goodwin** [(46050)](https://github.com/alchemy-run/alchemy-effect/commit/4605092) +- Implement Host, Self and make Lambda entrypoint  -  by **Sam Goodwin** [(1d9a2)](https://github.com/alchemy-run/alchemy-effect/commit/1d9a218) +- Generalize bundler and end to end deployment working  -  by **Sam Goodwin** [(65353)](https://github.com/alchemy-run/alchemy-effect/commit/65353fc) +- Namespace tree for organizing Resources and Bindings  -  by **Sam Goodwin** [(3b937)](https://github.com/alchemy-run/alchemy-effect/commit/3b93785) +- Default AWS StageConfig  -  by **Sam Goodwin** [(45152)](https://github.com/alchemy-run/alchemy-effect/commit/4515245) +- Bind Outputs to environment variables  -  by **Sam Goodwin** [(9ff75)](https://github.com/alchemy-run/alchemy-effect/commit/9ff7587) +- End-to-end bundling, bindings and IAM policies  -  by **Sam Goodwin** [(d2fef)](https://github.com/alchemy-run/alchemy-effect/commit/d2fef7a) +- Migrate to @distilled.cloud (v2)  -  by **Sam Goodwin** [(75151)](https://github.com/alchemy-run/alchemy-effect/commit/75151e7) +- Support external platforms  -  by **Sam Goodwin** [(7ed9f)](https://github.com/alchemy-run/alchemy-effect/commit/7ed9ffb) +- **aws**: + - DynamoDB Bindings and an AI process for implementing an aws service  -  by **Sam Goodwin** [(cdfd4)](https://github.com/alchemy-run/alchemy-effect/commit/cdfd45b) + - DynamoDB Table Stream  -  by **Sam Goodwin** [(ec61a)](https://github.com/alchemy-run/alchemy-effect/commit/ec61a6d) + - SNS Topic, Subscription, TopicSink and EventSource  -  by **Sam Goodwin** [(4d3af)](https://github.com/alchemy-run/alchemy-effect/commit/4d3af74) + - Cloudwatch Resources  -  by **Sam Goodwin** [(30544)](https://github.com/alchemy-run/alchemy-effect/commit/3054413) + - Add logs and tail for Lambda Functions  -  by **Sam Goodwin** [(8403c)](https://github.com/alchemy-run/alchemy-effect/commit/8403cda) +- **aws, dynamodb**: + - Add Batch and Transact Operations  -  by **Sam Goodwin** [(73a01)](https://github.com/alchemy-run/alchemy-effect/commit/73a0134) +- **aws, eventbridge**: + - EventBrdige Resources  -  by **Sam Goodwin** [(4809c)](https://github.com/alchemy-run/alchemy-effect/commit/4809c8e) +- **aws,acm**: + - Certificate  -  by **Sam Goodwin** [(c082f)](https://github.com/alchemy-run/alchemy-effect/commit/c082f83) +- **aws,autoscaling**: + - AutoScalingGroup, LaunchTemplate, ScalingPolicy  -  by **Sam Goodwin** [(d7878)](https://github.com/alchemy-run/alchemy-effect/commit/d787847) +- **aws,cloudfront**: + - Distribution, Function, Invalidation, KeyValueStore, OriginAccessControl  -  by **Sam Goodwin** [(2d2e5)](https://github.com/alchemy-run/alchemy-effect/commit/2d2e528) +- **aws,ec2**: + - EC2 Network and Instance  -  by **Sam Goodwin** [(8e686)](https://github.com/alchemy-run/alchemy-effect/commit/8e686d7) + - Share Hsot logic across EC2 Instance and LaunchTemplate  -  by **Sam Goodwin** [(691e2)](https://github.com/alchemy-run/alchemy-effect/commit/691e20b) +- **aws,ecr**: + - Repository Resource  -  by **Sam Goodwin** [(0af1e)](https://github.com/alchemy-run/alchemy-effect/commit/0af1ec1) +- **aws,ecs**: + - ECS Cluster, Task and Service Resources  -  by **Sam Goodwin** [(11c74)](https://github.com/alchemy-run/alchemy-effect/commit/11c7438) +- **aws,eks**: + - AWS Auto EKS Cluster resoruces  -  by **Sam Goodwin** [(9b4f9)](https://github.com/alchemy-run/alchemy-effect/commit/9b4f96f) +- **aws,elbv2**: + - Listener, LoadBalancer, TargetGroup  -  by **Sam Goodwin** [(8486e)](https://github.com/alchemy-run/alchemy-effect/commit/8486e65) + - Support TCP protocol in Listener, LoadBalancer  -  by **Sam Goodwin** [(b5128)](https://github.com/alchemy-run/alchemy-effect/commit/b5128d7) +- **aws,http**: + - Handle unrecoverable Http errors, fix GetObject IAM policy  -  by **Sam Goodwin** [(39e5c)](https://github.com/alchemy-run/alchemy-effect/commit/39e5c6c) +- **aws,iam**: + - AWS IAM Resources, Operations and Tests  -  by **Sam Goodwin** [(8638a)](https://github.com/alchemy-run/alchemy-effect/commit/8638abe) +- **aws,iamidentitycenter**: + - AccountAssignment, Group, Instance, PermissionSet  -  by **Sam Goodwin** [(3f864)](https://github.com/alchemy-run/alchemy-effect/commit/3f86467) +- **aws,kinesiss**: + - Operations for Kinesis Streams, including EventSource and Sinks  -  by **Sam Goodwin** [(8b40c)](https://github.com/alchemy-run/alchemy-effect/commit/8b40c72) +- **aws,lambda**: + - EventBridge and Stream Event Sources  -  by **Sam Goodwin** [(311a0)](https://github.com/alchemy-run/alchemy-effect/commit/311a04b) +- **aws,logs**: + - LogGroup Resource  -  by **Sam Goodwin** [(ff7e2)](https://github.com/alchemy-run/alchemy-effect/commit/ff7e2de) +- **aws,organizations**: + - Account, DelegatedAdministrator, Organization, OrganizationalUnit, OrganizationResourcePolicy, Policy, PolicyAttachment, Root, RootPolicyType, TrustedServiceAccess  -  by **Sam Goodwin** [(ed86a)](https://github.com/alchemy-run/alchemy-effect/commit/ed86a76) + - TenantRoot  -  by **Sam Goodwin** [(b6f7b)](https://github.com/alchemy-run/alchemy-effect/commit/b6f7b21) +- **aws,pipes**: + - Pipe resources  -  by **Sam Goodwin** [(38af3)](https://github.com/alchemy-run/alchemy-effect/commit/38af3e0) +- **aws,rds**: + - DB\* Resources, Aurora, Connect and RDSData  -  by **Sam Goodwin** [(9742b)](https://github.com/alchemy-run/alchemy-effect/commit/9742b8c) +- **aws,route53**: + - Record  -  by **Sam Goodwin** [(71d88)](https://github.com/alchemy-run/alchemy-effect/commit/71d88cd) +- **aws,scheduler**: + - Schedule, ScheduleGroup resources  -  by **Sam Goodwin** [(3be73)](https://github.com/alchemy-run/alchemy-effect/commit/3be73bd) +- **aws,secretsmanager**: + - Secret Resource and Bindings  -  by **Sam Goodwin** [(a8a39)](https://github.com/alchemy-run/alchemy-effect/commit/a8a39bd) +- **aws,sqs**: + - Allow DeleteMessageBatch, ReceiveMessage and SendMessage on EC2  -  by **Sam Goodwin** [(bddb4)](https://github.com/alchemy-run/alchemy-effect/commit/bddb4bb) +- **aws,website**: + - SsrSite, StaticSite, Router  -  by **Sam Goodwin** [(d4f14)](https://github.com/alchemy-run/alchemy-effect/commit/d4f14c3) + - Bring StaticSite up to par with SST  -  by **Sam Goodwin** [(6aaac)](https://github.com/alchemy-run/alchemy-effect/commit/6aaace9) +- **build**: + - Docker commands  -  by **Sam Goodwin** [(d12cf)](https://github.com/alchemy-run/alchemy-effect/commit/d12cf9f) +- **cli**: + - Tail and logs  -  by **Sam Goodwin** [(b0a06)](https://github.com/alchemy-run/alchemy-effect/commit/b0a061e) + - Filter logs and tail by resource ID  -  by **Sam Goodwin** [(a6514)](https://github.com/alchemy-run/alchemy-effect/commit/a6514c7) + - Process Manager daemon  -  by **Sam Goodwin** [(c0fd1)](https://github.com/alchemy-run/alchemy-effect/commit/c0fd148) +- **cloudflare**: + - Durable Objects and Worker entrypoint/exports  -  by **Sam Goodwin** [(aa6d3)](https://github.com/alchemy-run/alchemy-effect/commit/aa6d3a0) + - Durable Objects and Containers  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/37 [(ccade)](https://github.com/alchemy-run/alchemy-effect/commit/ccade21) + - DynamicWorker  -  by **Sam Goodwin** [(8c5d2)](https://github.com/alchemy-run/alchemy-effect/commit/8c5d205) + - Workflows  -  by **Sam Goodwin** [(874cc)](https://github.com/alchemy-run/alchemy-effect/commit/874cc9a) +- **core**: + - Handle circular binding cycles  -  by **Sam Goodwin** [(6d843)](https://github.com/alchemy-run/alchemy-effect/commit/6d843c4) + - Construct.fn and two-phase apply  -  by **Sam Goodwin** [(7c8bd)](https://github.com/alchemy-run/alchemy-effect/commit/7c8bd2b) + - Enhance convrergence of circularity in alchemy  -  by **Sam Goodwin** [(fbfbf)](https://github.com/alchemy-run/alchemy-effect/commit/fbfbf2a) + - Allow replace on top of a partially replaced resurce  -  by **Sam Goodwin** [(3c630)](https://github.com/alchemy-run/alchemy-effect/commit/3c63096) +- **k8s**: + - Kubernetes Manifest, Service, ServiceAccount, etc.  -  by **Sam Goodwin** [(8d2d1)](https://github.com/alchemy-run/alchemy-effect/commit/8d2d133) +- **kinesis**: + - Add AWS Kinesis Stream resource and capabilities  -  by **Sam Goodwin** and **Claude Haiku 4.5** in https://github.com/alchemy-run/alchemy-effect/issues/30 [(3f30b)](https://github.com/alchemy-run/alchemy-effect/commit/3f30b79) + +###    🐞 Bug Fixes + +- Improve deletion of ec2 resources  -  by **Sam Goodwin** [(c5bf6)](https://github.com/alchemy-run/alchemy-effect/commit/c5bf62f) +- Stack.make to capture providers and re-implement binding diffs in Plan  -  by **Sam Goodwin** [(94e40)](https://github.com/alchemy-run/alchemy-effect/commit/94e401c) +- Use error.\_tag instead of error.name  -  by **Sam Goodwin** [(87cbf)](https://github.com/alchemy-run/alchemy-effect/commit/87cbfa3) +- Handle undefined props  -  by **Sam Goodwin** [(63f08)](https://github.com/alchemy-run/alchemy-effect/commit/63f0869) +- Write bundle temp files to .alchemy/tmp  -  by **Sam Goodwin** [(0b491)](https://github.com/alchemy-run/alchemy-effect/commit/0b49156) +- Write temporary bundle file inside .alchemy within the correct module  -  by **Sam Goodwin** [(6d678)](https://github.com/alchemy-run/alchemy-effect/commit/6d678c8) +- Resource Service inherits call-site Context  -  by **Sam Goodwin** [(07511)](https://github.com/alchemy-run/alchemy-effect/commit/0751195) +- **aws**: + - Provide stack, stage and phase at runtime  -  by **Sam Goodwin** [(befb4)](https://github.com/alchemy-run/alchemy-effect/commit/befb4ff) + - Add missing Provider and Binding layers  -  by **Sam Goodwin** [(f41c1)](https://github.com/alchemy-run/alchemy-effect/commit/f41c13b) + - Add missing Providers  -  by **Sam Goodwin** [(82f51)](https://github.com/alchemy-run/alchemy-effect/commit/82f51ef) + - Bootstrap command creates and tags the bucket  -  by **Sam Goodwin** [(5a36f)](https://github.com/alchemy-run/alchemy-effect/commit/5a36fab) + - EC2 and LaunchTemplate host now provide ExecutionContext.Server  -  by **Sam Goodwin** [(e2f01)](https://github.com/alchemy-run/alchemy-effect/commit/e2f0100) + - Avoid redundant updates when source map changes  -  by **Sam Goodwin** [(48fe5)](https://github.com/alchemy-run/alchemy-effect/commit/48fe5cd) +- **aws,cloudflare**: + - Fix AnomalyDetector and bindings  -  by **Sam Goodwin** [(2d47e)](https://github.com/alchemy-run/alchemy-effect/commit/2d47e71) +- **aws,sns**: + - ReadSubscription returns undefined if topicArn is unknown  -  by **Sam Goodwin** [(4e25c)](https://github.com/alchemy-run/alchemy-effect/commit/4e25c3d) +- **aws,website**: + - Include stack, stage and FQN in kv namespace  -  by **Sam Goodwin** [(e971c)](https://github.com/alchemy-run/alchemy-effect/commit/e971ca7) + - End-to-end deployment of StaticSite  -  by **Sam Goodwin** [(1dea3)](https://github.com/alchemy-run/alchemy-effect/commit/1dea3ec) +- **cli**: + - Support running in node and load .env with ConfigProvider  -  by **Sam Goodwin** [(9b15b)](https://github.com/alchemy-run/alchemy-effect/commit/9b15b39) + - Support node and bun for globs  -  by **Sam Goodwin** [(2bcb6)](https://github.com/alchemy-run/alchemy-effect/commit/2bcb6d0) + - Respect caller's node runtime  -  by **Sam Goodwin** [(6396c)](https://github.com/alchemy-run/alchemy-effect/commit/6396cd6) +- **cloudflare**: + - Migrate to distilled-cloudflare and rolldown  -  by **Sam Goodwin** [(a4184)](https://github.com/alchemy-run/alchemy-effect/commit/a4184ec) + - Distilled 0.4.0 pagination apis  -  by **Sam Goodwin** [(4bffe)](https://github.com/alchemy-run/alchemy-effect/commit/4bffe41) + - Hard-code all exported handler methods instead of Proxy  -  by **Sam Goodwin** [(289b9)](https://github.com/alchemy-run/alchemy-effect/commit/289b92f) + - Detect changes to Container and Bundle  -  by **Sam Goodwin** [(924ff)](https://github.com/alchemy-run/alchemy-effect/commit/924ffb5) + - Pass assetsConfig in TanstackStart and StaticSite  -  by **Sam Goodwin** [(42832)](https://github.com/alchemy-run/alchemy-effect/commit/428329f) + - Get HTTP server plumbing working for containers  -  by **Sam Goodwin** [(cf058)](https://github.com/alchemy-run/alchemy-effect/commit/cf05888) +- **cloudflare,assets**: + - Pass jwtToken to createAssetUpload  -  by **Sam Goodwin** [(26228)](https://github.com/alchemy-run/alchemy-effect/commit/262289d) +- **core**: + - Accept Input in Host props  -  by **Sam Goodwin** [(09edc)](https://github.com/alchemy-run/alchemy-effect/commit/09edccf) + - Add asEffect to Output  -  by **Sam Goodwin** [(5ec8d)](https://github.com/alchemy-run/alchemy-effect/commit/5ec8d27) +- **ec2**: + - Query attachments when deleting IGW  -  by **Sam Goodwin** [(ced7f)](https://github.com/alchemy-run/alchemy-effect/commit/ced7f85) +- **process**: + - Export Process module  -  by **Sam Goodwin** [(1600d)](https://github.com/alchemy-run/alchemy-effect/commit/1600d3c) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.6.0...v0.6.1) + +--- + +## v0.6.0 + +###    🚀 Features + +- **ec2**: NAT Gateway, EIP, Egress IGW, SecurityGroups  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/24 [(ff04d)](https://github.com/alchemy-run/alchemy-effect/commit/ff04d57) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.5.0...v0.6.0) + +--- + +## v0.5.0 + +###    🚀 Features + +- Standardize physical name generation  -  by **Sam Goodwin** [(91199)](https://github.com/alchemy-run/alchemy-effect/commit/9119967) +- **cloudflare**: + - Rename worker.id and name workerId and workerName  -  by **Sam Goodwin** [(273cb)](https://github.com/alchemy-run/alchemy-effect/commit/273cb26) + - Rename Bucket.name to Bucket.bucketName and constraint name length  -  by **Sam Goodwin** [(a0733)](https://github.com/alchemy-run/alchemy-effect/commit/a0733a6) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.4.0...v0.5.0) + +--- + +## v0.4.0 + +###    🚀 Features + +- **ec2**: Add IGW, Route, Route Table, Route Table Assoication  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/23 [(0eeb6)](https://github.com/alchemy-run/alchemy-effect/commit/0eeb613) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.3.0...v0.4.0) + +--- + +## v0.3.0 + +###    🚀 Features + +- **core**: + - Inputs and Outputs  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/19 [(fa8b8)](https://github.com/alchemy-run/alchemy-effect/commit/fa8b893) + - DefineStack, defineStages and alchemy CLI  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/21 [(d30da)](https://github.com/alchemy-run/alchemy-effect/commit/d30da72) + +###    🐞 Bug Fixes + +- **cloudflare**: Make props optional in KV Namespace and R2 Bucket  -  by **Sam Goodwin** [(dbfbe)](https://github.com/alchemy-run/alchemy-effect/commit/dbfbe40) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.2.0...v0.3.0) + +--- + +## v0.2.0 + +###    🚀 Features + +- Diff bindings + Queue Event Source  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/12 [(f0ba8)](https://github.com/alchemy-run/alchemy-effect/commit/f0ba897) +- **aws**: + - DynamoDB Table and getItem  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/10 [(877ba)](https://github.com/alchemy-run/alchemy-effect/commit/877ba8f) + - VPC  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/16 [(cda86)](https://github.com/alchemy-run/alchemy-effect/commit/cda86ce) +- **cloudflare**: + - Worker, Assets, R2, KV  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/13 [(6e5b1)](https://github.com/alchemy-run/alchemy-effect/commit/6e5b107) +- **test**: + - Add test utility  -  by **Sam Goodwin** in https://github.com/alchemy-run/alchemy-effect/issues/15 [(7a9e1)](https://github.com/alchemy-run/alchemy-effect/commit/7a9e10f) + +###    🐞 Bug Fixes + +- Properly type the Resource Provider Layers and use Layer.merge  -  by **Sam Goodwin** [(77d69)](https://github.com/alchemy-run/alchemy-effect/commit/77d69be) +- **core**: Exclude bindings from props diff and include attributes in no-op bind  -  by **John Royal** in https://github.com/alchemy-run/alchemy-effect/issues/17 [(6408d)](https://github.com/alchemy-run/alchemy-effect/commit/6408d2b) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v0.1.0...v0.2.0) + +--- + +## v0.1.0 + +###    🚀 Features + +- Adopt currying pattern across codebase to deal with NoInfer and Extract limitations  -  by **Sam Goodwin** [(9a319)](https://github.com/alchemy-run/alchemy-effect/commit/9a3193c) +- Remove HKTs from capability and resource  -  by **Sam Goodwin** [(88a59)](https://github.com/alchemy-run/alchemy-effect/commit/88a594a) +- Use triples in Policy to support overriden tags for bindings  -  by **Sam Goodwin** [(9e8fc)](https://github.com/alchemy-run/alchemy-effect/commit/9e8fc77) +- Introduce BindingTag to map Binding -> BindingService  -  by **Sam Goodwin** [(c59b7)](https://github.com/alchemy-run/alchemy-effect/commit/c59b76c) +- Capture Capability ID in Binding declaration  -  by **Sam Goodwin** [(742c2)](https://github.com/alchemy-run/alchemy-effect/commit/742c205) +- Standardize .provider builder pattern and add 'binding' integration contract type to Runtime  -  by **Sam Goodwin** [(6afdd)](https://github.com/alchemy-run/alchemy-effect/commit/6afdd33) +- Include Phase in the Plan and properly type the output of Apply  -  by **Sam Goodwin** [(1eade)](https://github.com/alchemy-run/alchemy-effect/commit/1eade78) +- Add provider: { effect, succeed } to Resources  -  by **Sam Goodwin** [(c4233)](https://github.com/alchemy-run/alchemy-effect/commit/c42336f) +- Thread BindNode through state and fix the planner  -  by **Sam Goodwin** [(c1bb3)](https://github.com/alchemy-run/alchemy-effect/commit/c1bb313) +- Apply bindings in the generic apply functions instead of each provider  -  by **Sam Goodwin** [(f0f34)](https://github.com/alchemy-run/alchemy-effect/commit/f0f348a) + +###    🐞 Bug Fixes + +- Bind types  -  by **Sam Goodwin** [(d11c7)](https://github.com/alchemy-run/alchemy-effect/commit/d11c774) +- Update Instance to handle the resource types like Queue  -  by **Sam Goodwin** [(6adca)](https://github.com/alchemy-run/alchemy-effect/commit/6adca3c) +- Re-work simpler types for plan and apply  -  by **Sam Goodwin** [(1d08d)](https://github.com/alchemy-run/alchemy-effect/commit/1d08d19) +- Instance maps to Queue instead of Resource<..>  -  by **Sam Goodwin** [(aa18f)](https://github.com/alchemy-run/alchemy-effect/commit/aa18f3a) +- Pass-through of the Props and Bindings to the Service type  -  by **Sam Goodwin** [(9784e)](https://github.com/alchemy-run/alchemy-effect/commit/9784eca) +- Remove Resource from Binding tag construction and implement layer builders  -  by **Sam Goodwin** [(ada7c)](https://github.com/alchemy-run/alchemy-effect/commit/ada7cba) +- Plumb through new Plan structure to apply and CLI  -  by **Sam Goodwin** [(8c057)](https://github.com/alchemy-run/alchemy-effect/commit/8c0570b) +- Missing props in Capability, Bindingm, Resource and Service  -  by **Sam Goodwin** [(aaec3)](https://github.com/alchemy-run/alchemy-effect/commit/aaec377) +- Include bindings in the plan, fix papercuts, remove node:util usage  -  by **Sam Goodwin** [(0a5f4)](https://github.com/alchemy-run/alchemy-effect/commit/0a5f4d3) +- Log message when SSO token has expired  -  by **Sam Goodwin** [(8a7a7)](https://github.com/alchemy-run/alchemy-effect/commit/8a7a72f) +- Infer return type of Resource.provider.effect  -  by **Sam Goodwin** [(2093f)](https://github.com/alchemy-run/alchemy-effect/commit/2093f17) +- Serialize classes properly and rename packages  -  by **Sam Goodwin** [(2bf72)](https://github.com/alchemy-run/alchemy-effect/commit/2bf7290) + +#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/c46b447ca0d46a9e4dbf08a6789770a420d90be5...v0.1.0) + +--- diff --git a/.repos/alchemy-effect/CLAUDE.md b/.repos/alchemy-effect/CLAUDE.md new file mode 100644 index 00000000000..43c994c2d36 --- /dev/null +++ b/.repos/alchemy-effect/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/.repos/alchemy-effect/README.md b/.repos/alchemy-effect/README.md new file mode 100644 index 00000000000..8168723fc5f --- /dev/null +++ b/.repos/alchemy-effect/README.md @@ -0,0 +1,85 @@ +
+ + + Alchemy — Infrastructure as Effects + + +
+ +[![npm](https://img.shields.io/npm/v/alchemy?style=flat-square&color=3f5a2a&label=alchemy)](https://www.npmjs.com/package/alchemy) +[![license](https://img.shields.io/badge/license-Apache%202.0-3f5a2a?style=flat-square)](./LICENSE) +[![discord](https://img.shields.io/badge/discord-join-3f5a2a?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/jwKw8dBJdN) + +**Infrastructure-as-Effects** — cloud infrastructure and application logic as a single, type-safe [Effect](https://effect.website) program. + +[Docs](https://v2.alchemy.run) · [Tutorial](https://v2.alchemy.run/tutorial/part-1) · [Examples](./examples) · [Discord](https://discord.gg/jwKw8dBJdN) + +
+ +--- + +A Worker bound to a R2 bucket and serving objects from it: + +```typescript +const Bucket = Cloudflare.R2Bucket("bucket"); + +export default Cloudflare.Worker( + "api", + { main: import.meta.path }, + Effect.gen(function* () { + const bucket = yield* Cloudflare.R2Bucket.bind(Bucket); + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const object = yield* bucket.get(request.url); + return HttpServerResponse.stream(object!.body); + }) + }; + }), +); +``` + +One `bind()` wires the binding, env var, and typed connection — at deploy time and at runtime. + +--- + +- **One program, one language.** Resources, Lambdas/Workers, IAM, and SDKs live in the same Effect program — no YAML, no second runtime. +- **Bindings, not glue code.** `S3.GetObject.bind(bucket)` wires the IAM policy, env var, and a typed SDK call in a single line. +- **Errors in the type system.** Every cloud API failure is a tagged Effect error you handle — or don't — on purpose. +- **AWS + Cloudflare today.** S3, SQS, DynamoDB, Kinesis, Lambda, EC2 / Workers, R2, D1, Durable Objects, Containers. +- **Same code, every stage.** Local dev, `plan` / `deploy`, smoke tests, and CI all share one mental model. + +```sh +bun add alchemy effect +``` + +## Bootstrap with an AI coding agent + +Paste this into Claude Code, Cursor, or any agent that can fetch a URL: + +``` +You are an Alchemy expert. Read https://v2.alchemy.run/llms.txt to load the +full documentation index, then act as my pair on this project. + +Goal: help me set up, build, test, and deploy a cloud application with +`alchemy` (Infrastructure-as-Effects, powered by Effect). + +Follow the patterns from the docs and the /examples folder. Stay idiomatic +to Effect: use Layers for wiring, return Effects from lifecycle code, and +keep infra and runtime in the same program. Ask before introducing new +dependencies or breaking conventions. +``` + +## Learn more + +- [What is Alchemy?](https://v2.alchemy.run/what-is-alchemy) — the framework in 2 minutes +- [Getting Started](https://v2.alchemy.run/getting-started) — your first Stack +- [Tutorial](https://v2.alchemy.run/tutorial/part-1) — five-part walkthrough to a tested, CI-deployed app +- [Examples](./examples) — runnable projects on AWS and Cloudflare +- [llms.txt](https://v2.alchemy.run/llms.txt) — agent-ready documentation index + +> **alchemy** is in alpha. Expect breaking changes. Come hang in our [Discord](https://discord.gg/jwKw8dBJdN). + +## License + +Apache-2.0 diff --git a/.repos/alchemy-effect/bun.lock b/.repos/alchemy-effect/bun.lock new file mode 100644 index 00000000000..4f05e793e75 --- /dev/null +++ b/.repos/alchemy-effect/bun.lock @@ -0,0 +1,5316 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "alchemy-monorepo", + "devDependencies": { + "@alchemy.run/pr-package": "workspace:*", + "@ark/attest": "^0.56.0", + "@cloudflare/puppeteer": "^1.1.0", + "@distilled.cloud/cloudflare": "catalog:", + "@effect/language-service": "^0.77.0", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@types/bun": "latest", + "@types/node": "latest", + "@typescript/native-preview": "^7.0.0-dev.20260317.1", + "@vitest/ui": "^4.1.0", + "alchemy": "workspace:*", + "bun-types": "^1.3.8", + "changelogithub": "^13.16.1", + "dotenv": "^17.2.3", + "effect": "catalog:", + "husky": "^9.1.7", + "oxfmt": "latest", + "oxlint": "latest", + "pkg-pr-new": "^0.0.62", + "ts-morph": "^27.0.2", + "typescript": "latest", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "^4.0.18", + "yaml": "^2.8.2", + }, + }, + "examples/aws-ec2": { + "name": "aws-ec2-example", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-ecs": { + "name": "aws-ecs-example", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-eks": { + "name": "aws-eks-example", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-lambda": { + "name": "aws-lambda", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-lambda-httpapi": { + "name": "aws-lambda-httpapi", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-lambda-rpc": { + "name": "aws-lambda-rpc", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-rds": { + "name": "aws-rds-example", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "@types/pg": "^8.18.0", + "alchemy": "workspace:*", + "effect": "catalog:", + "pg": "^8.20.0", + }, + }, + "examples/aws-rest-api": { + "name": "aws-rest-api", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-static-site": { + "name": "aws-static-site-example", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/aws-vite": { + "name": "aws-vite-example", + "version": "0.0.0", + "dependencies": { + "@cloudflare/vite-plugin": "catalog:", + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + "devDependencies": { + "typescript": "catalog:", + "vite": "catalog:", + }, + }, + "examples/cloudflare-dev": { + "name": "cloudflare-dev", + "version": "0.0.0", + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/cloudflare-email": { + "name": "cloudflare-email", + "version": "0.0.0", + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/cloudflare-git-artifacts": { + "name": "cloudflare-git-artifacts", + "version": "0.0.0", + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/cloudflare-neon-drizzle": { + "name": "cloudflare-neon-drizzle", + "version": "0.0.0", + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@effect/sql-pg": "catalog:", + "alchemy": "workspace:*", + "drizzle-orm": "1.0.0-rc.1", + "effect": "catalog:", + "pg": "^8.13.0", + }, + "devDependencies": { + "@types/pg": "^8.11.0", + "drizzle-kit": "1.0.0-rc.1", + }, + }, + "examples/cloudflare-planetscale-mysql-drizzle": { + "name": "cloudflare-planetscale-mysql-drizzle", + "version": "0.0.0", + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "drizzle-orm": "1.0.0-rc.1", + "effect": "catalog:", + "mysql2": "^3.15.3", + }, + "devDependencies": { + "drizzle-kit": "1.0.0-rc.1", + }, + }, + "examples/cloudflare-planetscale-postgres-drizzle": { + "name": "cloudflare-planetscale-postgres-drizzle", + "version": "0.0.0", + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@effect/sql-pg": "catalog:", + "alchemy": "workspace:*", + "drizzle-orm": "1.0.0-rc.1", + "effect": "catalog:", + "pg": "^8.13.0", + }, + "devDependencies": { + "@types/pg": "^8.11.0", + "drizzle-kit": "1.0.0-rc.1", + }, + }, + "examples/cloudflare-secrets-store": { + "name": "cloudflare-secrets-store", + "version": "0.0.0", + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/cloudflare-solidjs-ssr": { + "name": "cloudflare-solid-ssr", + "version": "0.0.0", + "dependencies": { + "@solidjs/router": "^0.15.1", + "solid-js": "^1.9.5", + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.13", + "alchemy": "workspace:*", + "effect": "catalog:", + "postcss": "^8.4.49", + "solid-devtools": "^0.34.3", + "tailwindcss": "^4.1.13", + "vite": "^7.1.4", + "vite-plugin-solid": "^2.11.8", + }, + }, + "examples/cloudflare-solidstart": { + "name": "example-basic", + "dependencies": { + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.15.0", + "@solidjs/start": "2.0.0-alpha.2", + "@solidjs/vite-plugin-nitro-2": "^0.1.0", + "alchemy": "workspace:*", + "effect": "catalog:", + "solid-js": "^1.9.5", + "vite": "^7.0.0", + }, + }, + "examples/cloudflare-static-site": { + "name": "cloudflare-vite-example", + "version": "0.0.0", + "dependencies": { + "@distilled.cloud/cloudflare": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + "devDependencies": { + "typescript": "catalog:", + "vite": "catalog:", + }, + }, + "examples/cloudflare-tanstack": { + "name": "cloudflare-tanstack-example", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-router": "^1.167.4", + "@tanstack/react-start": "^1.166.15", + "alchemy": "workspace:*", + "effect": "catalog:", + "react": "^19.2.4", + "react-dom": "^19.2.4", + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "^5.9.3", + "vite": "catalog:", + }, + }, + "examples/cloudflare-tanstack-start-solid": { + "name": "cloudflare-tanstack-start-solid-example", + "version": "0.0.0", + "dependencies": { + "@tanstack/solid-router": "^1.170.4", + "@tanstack/solid-start": "^1.168.6", + "alchemy": "workspace:*", + "effect": "catalog:", + "solid-js": "^1.9.12", + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "@types/bun": "latest", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plugin-solid": "^2.11.12", + }, + }, + "examples/cloudflare-vue": { + "name": "cloudflare-vue", + "version": "0.0.0", + "dependencies": { + "vue": "beta", + "vue-router": "^5.0.4", + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.1", + "alchemy": "workspace:*", + "effect": "catalog:", + "npm-run-all2": "^8.0.4", + "oxfmt": "^0.42.0", + "typescript": "~6.0.0", + "vite": "^8.0.3", + "vite-plugin-vue-devtools": "^8.1.1", + "vue-tsc": "^3.2.6", + }, + }, + "examples/cloudflare-worker": { + "name": "cloudflare-worker", + "version": "0.0.0", + "dependencies": { + "@alchemy.run/better-auth": "workspace:*", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "better-auth": "catalog:", + "effect": "catalog:", + }, + }, + "examples/cloudflare-worker-async": { + "name": "cloudflare-worker-async", + "version": "0.0.0", + "dependencies": { + "@alchemy.run/better-auth": "workspace:*", + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "better-auth": "catalog:", + "effect": "catalog:", + }, + }, + "examples/monorepo-multi-stack/backend": { + "name": "@monorepo-multi-stack/backend", + "version": "0.0.0", + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/monorepo-multi-stack/frontend": { + "name": "@monorepo-multi-stack/frontend", + "version": "0.0.0", + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@monorepo-multi-stack/backend": "workspace:*", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "alchemy": "workspace:*", + "effect": "catalog:", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "vite": "catalog:", + }, + }, + "examples/monorepo-single-stack/backend": { + "name": "@monorepo-single-stack/backend", + "version": "0.0.0", + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "examples/monorepo-single-stack/frontend": { + "name": "@monorepo-single-stack/frontend", + "version": "0.0.0", + "dependencies": { + "@monorepo-single-stack/backend": "workspace:*", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "alchemy": "workspace:*", + "effect": "catalog:", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "vite": "catalog:", + }, + }, + "packages/alchemy": { + "name": "alchemy", + "version": "2.0.0-beta.48", + "bin": { + "alchemy": "./bin/cli.js", + }, + "dependencies": { + "@alchemy.run/node-utils": "catalog:", + "@aws-sdk/credential-providers": "catalog:", + "@clack/prompts": "^0.11.0", + "@distilled.cloud/aws": "catalog:", + "@distilled.cloud/axiom": "catalog:", + "@distilled.cloud/cloudflare": "catalog:", + "@distilled.cloud/cloudflare-rolldown-plugin": "catalog:", + "@distilled.cloud/cloudflare-runtime": "catalog:", + "@distilled.cloud/cloudflare-vite-plugin": "catalog:", + "@distilled.cloud/core": "catalog:", + "@distilled.cloud/neon": "catalog:", + "@distilled.cloud/planetscale": "catalog:", + "@effect/vitest": "catalog:", + "@libsql/client": "catalog:", + "@octokit/rest": "^22.0.1", + "@smithy/node-config-provider": "catalog:", + "@smithy/shared-ini-file-loader": "catalog:", + "@smithy/types": "catalog:", + "@types/aws-lambda": "catalog:", + "aws4fetch": "catalog:", + "capnweb": "^0.6.1", + "fast-glob": "^3.3.2", + "fast-xml-parser": "catalog:", + "ink": "^6.3.1", + "jszip": "^3.10.1", + "libsodium-wrappers": "^0.8.3", + "magic-string": "^0.30.21", + "mysql2": "^3.15.3", + "pathe": "^2.0.3", + "pg": "^8.13.0", + "picomatch": "^4.0.4", + "react": "^19.2.0", + "rolldown": "catalog:", + "undici": "^7.16.0", + "yaml": "catalog:", + }, + "devDependencies": { + "@clack/prompts": "^0.11.0", + "@cloudflare/puppeteer": "^1.1.0", + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@effect/platform-node-shared": "catalog:", + "@oxc-project/types": "^0.127.0", + "@types/aws-lambda": "catalog:", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@types/pg": "^8.11.0", + "@types/picomatch": "^4.0.0", + "@types/react": "^19.2.2", + "@types/ws": "^8.18.1", + "better-auth": "catalog:", + "effect": "catalog:", + "react-devtools-core": "^7.0.1", + "solid-js": "catalog:", + "tsconfig-paths": "^4.2.0", + "tsdown": "^0.15.4", + "tsx": "^4.20.6", + "typescript": "catalog:", + "ws": "catalog:", + }, + "peerDependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@effect/sql-pg": "catalog:", + "drizzle-kit": "catalog:", + "drizzle-orm": "catalog:", + "effect": "catalog:", + "vite": "catalog:", + "ws": "catalog:", + }, + "optionalPeers": [ + "@effect/platform-bun", + "@effect/platform-node", + "@effect/sql-pg", + "drizzle-kit", + "drizzle-orm", + "vite", + "ws", + ], + }, + "packages/better-auth": { + "name": "@alchemy.run/better-auth", + "version": "2.0.0-beta.48", + "dependencies": { + "alchemy": "workspace:*", + "effect": "catalog:", + }, + "peerDependencies": { + "better-auth": "catalog:", + }, + }, + "packages/pr-package": { + "name": "@alchemy.run/pr-package", + "version": "2.0.0-beta.48", + "dependencies": { + "alchemy": "workspace:*", + "effect": "catalog:", + }, + }, + "website": { + "name": "website", + "version": "0.0.0", + "dependencies": { + "@astrojs/check": "^0.9.4", + "@astrojs/mdx": "^5.0.3", + "@astrojs/react": "^5.0.3", + "@astrojs/sitemap": "^3.5.0", + "@astrojs/starlight": "^0.34.3", + "@astrojs/starlight-tailwind": "^4.0.1", + "@effect/platform-node": "catalog:", + "@iconify-json/logos": "^1.2.11", + "@iconify-json/lucide": "^1.2.102", + "@iconify/react": "^6.0.2", + "@resvg/resvg-js": "^2.6.2", + "@tailwindcss/vite": "^4.1.14", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "alchemy": "workspace:*", + "astro": "^5.12.8", + "effect": "catalog:", + "expressive-code-twoslash": "^0.6.1", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-icons": "^5.6.0", + "rehype-mermaid": "^3.0.0", + "satori": "^0.26.0", + "starlight-blog": "0.24.0", + "tailwindcss": "^4.1.14", + }, + "devDependencies": { + "astro-broken-links-checker": "^1.1.0", + "typescript": "catalog:", + "zod": "^3", + }, + }, + }, + "catalog": { + "@alchemy.run/node-utils": "0.0.4", + "@aws-sdk/credential-providers": "^3.0.0", + "@cloudflare/containers": "^0.1.1", + "@cloudflare/vite-plugin": "^1.13.12", + "@cloudflare/workers-types": "^4.20250805.0", + "@distilled.cloud/aws": "^0.22.4", + "@distilled.cloud/axiom": "^0.22.4", + "@distilled.cloud/cloudflare": "^0.22.4", + "@distilled.cloud/cloudflare-rolldown-plugin": "0.10.1", + "@distilled.cloud/cloudflare-runtime": "0.10.1", + "@distilled.cloud/cloudflare-vite-plugin": "0.10.1", + "@distilled.cloud/core": "^0.22.4", + "@distilled.cloud/neon": "^0.22.4", + "@distilled.cloud/planetscale": "^0.22.4", + "@effect/language-service": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/platform-bun": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/platform-node": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/platform-node-shared": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/sql-pg": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/vitest": ">=4.0.0-beta.74 || >=4.0.0", + "@libsql/client": "^0.17.0", + "@opentui/core": "latest", + "@opentui/solid": "latest", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "@types/aws-lambda": "^8.10.152", + "@types/bun": "latest", + "@types/node": "latest", + "@typescript/native-preview": "latest", + "ai": "^6.0.62", + "aws4fetch": "^1.0.20", + "better-auth": "^1.6.2", + "drizzle-kit": ">=1.0.0-rc.1", + "drizzle-orm": ">=1.0.0-rc.1", + "effect": ">=4.0.0-beta.74 || >=4.0.0", + "fast-xml-parser": "^5.3.4", + "rolldown": "1.0.1", + "solid-js": "latest", + "sonda": "^0.11.1", + "typescript": "^6.0.3", + "vite": "^8.0.7", + "web-tree-sitter": "0.25.10", + "ws": "^8.20.0", + "yaml": "^2.0.0", + }, + "packages": { + "@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="], + + "@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="], + + "@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + + "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], + + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], + + "@alchemy.run/better-auth": ["@alchemy.run/better-auth@workspace:packages/better-auth"], + + "@alchemy.run/node-utils": ["@alchemy.run/node-utils@0.0.4", "", {}, "sha512-TiIhPXCTCi3tk0zmdYJJ14CNSesSfsJxXdIOP0HTSItQ1mZWLocrF7qCuEWKyW/IEFzp6kaiOf19aIA/mbCp1g=="], + + "@alchemy.run/pr-package": ["@alchemy.run/pr-package@workspace:packages/pr-package"], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + + "@ark/attest": ["@ark/attest@0.56.0", "", { "dependencies": { "@ark/fs": "0.56.0", "@ark/util": "0.56.0", "@prettier/sync": "0.6.1", "@typescript/analyze-trace": "0.10.1", "@typescript/vfs": "1.6.1", "arktype": "2.1.28", "prettier": "3.6.2" }, "peerDependencies": { "typescript": "*" }, "bin": { "attest": "out/cli/cli.js" } }, "sha512-Pngs8f1UJiWbeO8LKVdyBL0K3rT/PDVACR7TPyCEULNcN8V1rVec6dKvUnQVpQSj60p4ejlK6dKs+fQoqdUMqA=="], + + "@ark/fs": ["@ark/fs@0.56.0", "", {}, "sha512-zY/wDDhcvmt6/upQwZM766PAnvIzdEMcgydUGd9pqY9FMGNo9I9uE4RYAfms9AeUUtbZJu2h2Ua0tvFsO5XF4Q=="], + + "@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="], + + "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], + + "@astrojs/check": ["@astrojs/check@0.9.9", "", { "dependencies": { "@astrojs/language-server": "^2.16.7", "chokidar": "^4.0.3", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg=="], + + "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], + + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw=="], + + "@astrojs/language-server": ["@astrojs/language-server@2.16.10", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.4", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", "volar-service-css": "0.0.70", "volar-service-emmet": "0.0.70", "volar-service-html": "0.0.70", "volar-service-prettier": "0.0.70", "volar-service-typescript": "0.0.70", "volar-service-typescript-twoslash-queries": "0.0.70", "volar-service-yaml": "0.0.70", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "./bin/nodeServer.js" } }, "sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w=="], + + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.9.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q=="], + + "@astrojs/mdx": ["@astrojs/mdx@5.0.6", "", { "dependencies": { "@astrojs/markdown-remark": "7.1.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw=="], + + "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], + + "@astrojs/react": ["@astrojs/react@5.0.6", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@vitejs/plugin-react": "^5.2.0", "devalue": "^5.6.4", "ultrahtml": "^1.6.0", "vite": "^7.3.2" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, "sha512-3pfjmw3sUnV5WplLblDzsAKlHv9kNmlCFAw/xP/agubiZFo4d+uPXX2jynNwAfvwyI5v+Q9uIIFwyujv9jifLg=="], + + "@astrojs/rss": ["@astrojs/rss@4.0.18", "", { "dependencies": { "fast-xml-parser": "^5.5.7", "piccolore": "^0.1.3", "zod": "^4.3.6" } }, "sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg=="], + + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="], + + "@astrojs/starlight": ["@astrojs/starlight@0.34.8", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.41.1", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.1", "hast-util-select": "^6.0.2", "hast-util-to-string": "^3.0.0", "hastscript": "^9.0.0", "i18next": "^23.11.5", "js-yaml": "^4.1.0", "klona": "^2.0.6", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.0", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" }, "peerDependencies": { "astro": "^5.5.0" } }, "sha512-XuYz0TfCZhje2u1Q9FNtmTdm7/B9QP91RDI1VkPgYvDhSYlME3k8gwgcBMHnR9ASDo2p9gskrqe7t1Pub/qryg=="], + + "@astrojs/starlight-tailwind": ["@astrojs/starlight-tailwind@4.0.2", "", { "peerDependencies": { "@astrojs/starlight": ">=0.34.0", "tailwindcss": "^4.0.0" } }, "sha512-SYN/6zq6hJO5tWqbQ2tWT9/jd8ubUkzkBCcF94vByC/ZJ20Mi5GPjFvAh89Yky/aIM+jXxT6W5q4p6l58GKHiQ=="], + + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="], + + "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], + + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="], + + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.38", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-OHkK6xOx/IHkSbQdDWxnVCLU+j28EFl8wyWgBILQDFAPY8n240C/O4gjmFx+zFU12lL8njgJQ5GWAIWq88CnSQ=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.43", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.41", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/token-providers": "3.1056.0", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.45", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1057.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1057.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.26", "", { "dependencies": { "@smithy/types": "^4.14.2", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@8.0.0-rc.6", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.6", "@babel/types": "^8.0.0-rc.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-decorators": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg=="], + + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@better-auth/core": ["@better-auth/core@1.6.12", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-6mXtYSYfo6TvHHCZAZmfjvIQQtBDWzWzwy9iIWPEoede2lP2SuJzkfIQNuTtIGzZcn7a9iuzIm1jWDBzfnBARg=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-g0sKQstvXHH70s+TjAXo86cNyWV60ahhJm1sow27RyW41U10vfBehOFinU3GPESyxl/fEr9D27rk3jdl6E3l3A=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-KhPwPmLj+MoTVGV6goPfCYf/7Fuiy2Q37GEWhvQdoFjkYKbGo995OoghBVNBnAYOakYvTYjG0JebCfiETBVX3g=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1" } }, "sha512-flblsePBCcB0DA6hewAOupxyypNTQczZvkNYvRrsVlBDIh0+vHBU/dTjoDmuQnZ3egTdFNnMeC+VrNnqt/GFUg=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-IeiHZN9PtIyiqYgTDlrmm8sYI++5p1OI49uWB7LHg2+touiaNUGe0uWYymQpw1zq1e8FJxKlwvOc5vw6nGrI6g=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-+GvU8vZ3aJUHDBuR5PxtU5OpPQS2T9ND7s2JYm63bD6rnYztLwEo8bwHL3BvsTwSvCjFHZCtsn1A+6qyoOzTMw=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.12", "", { "peerDependencies": { "@better-auth/core": "^1.6.12", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21" } }, "sha512-g59qLPq9SROyku0X5tiZpXXiVrsbjB1QA6OctOt9svzj7NjCFBoCAO9QlBiOTUolo0l9CF6fLlc85PoBkY5RtA=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.1", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-SZBPRPF3z0nBvE5ygOkxae35wnnXPRShmqFo78S+qslLeFoPu/pMgnXAuNKFMMybac3tiLaVg1e3MQW5MC+1iA=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], + + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + + "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + + "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@cloudflare/puppeteer": ["@cloudflare/puppeteer@1.1.0", "", { "dependencies": { "@puppeteer/browsers": "2.2.4", "debug": "^4.3.5", "devtools-protocol": "0.0.1299070", "ws": "^8.18.0" } }, "sha512-lN10En49avRDQvz8Gpv/WiIoGvjjDiP6P+v2y9bA6rfPT5WHfnlg2O6MwjHc1POm8A9LXf1sMdgrsdW8xWapOw=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.39.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260526.0", "unenv": "2.0.0-rc.24", "wrangler": "4.95.0", "ws": "8.20.1" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" } }, "sha512-AHC+KSR+3dtGu7Ab7I0Ode4Whx12TxMEmiZt7w+Fc3/2wYNByIzbb6cndWZ78tnveFdO1xhNLv1YaNngxGtOPg=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260526.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/pR3GH3gfv0PUp7DjI8v0aAIDOqFwibq4bg5xT7TZgcVdBV/cJQWckdXCMqiRtHiawLwogUX00EIOINkYJ1Zqg=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260526.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rcyu0iANYfaiezKh3Mcao1O4IIgVfQldxduiL5TZT1sP0NIeRY4YReSTrzPxNnXxSYaIqaqRHMcHbUM/ic4knA=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260526.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5EZAEnlLwa9oGJRo8Nd3iY5Wcd9ROGNNG90xNIGp8MEjj8v2jTn42NC47fCZKFdnLj3+S+vWEhu1x0GVJnALjA=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260526.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-X/YBQXeXFeCN7QTStoWrATEBc9WKl7PIqkw/dQkjyJ72gh3rkLe0+Xkzp3wO7gtxTDQMa7NPGy1W4+sdMf8q1g=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260526.1", "", { "os": "win32", "cpu": "x64" }, "sha512-R+tqpFFdcfZIljx8fIW9rj9fRTtDgfoA2yonsfAGa6e8snrmr+38mdFHtkRC0D3UyZpn/hOtmXiUBfdX2gMR7Q=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260529.1", "", {}, "sha512-33n3nsaWELSgn4DLKj1X9dwZc3kVDnO+jF/hLH9fdaXG9mQzKDeUkQaVRWLJXvrPXPa9RaIuSAFO4Zh9YOqOog=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], + + "@distilled.cloud/aws": ["@distilled.cloud/aws@0.22.4", "", { "dependencies": { "@aws-crypto/crc32": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/credential-providers": "^3.994.0", "@aws-sdk/types": "^3.973.1", "@distilled.cloud/core": "0.22.4", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "aws4fetch": "^1.0.20", "fast-xml-parser": "^5.3.2" }, "peerDependencies": { "effect": ">=4.0.0-beta.66 || >=4.0.0" } }, "sha512-qa+MnIdlRy6QmXuRwGqBNQnF2mu9MyUck2yeYkEesDpOH4eJFcjZy8nVUIhWxACfSIMvLd59FD6bkdVALzI5Dg=="], + + "@distilled.cloud/axiom": ["@distilled.cloud/axiom@0.22.4", "", { "dependencies": { "@distilled.cloud/core": "0.22.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.66 || >=4.0.0" } }, "sha512-CcSe8UPEPLGW/tGZ/ky5Hn5Gfd2/43bn5km6opeiyN45HrHbkvYURQeiqgcivJt2Y01Mh0lMeBkQQdfCjJIhjA=="], + + "@distilled.cloud/cloudflare": ["@distilled.cloud/cloudflare@0.22.4", "", { "dependencies": { "@distilled.cloud/core": "0.22.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.66 || >=4.0.0" } }, "sha512-hDqVoGmJlPoGCgurVFbk399gkPyu9Pr/w8D4Rv/q5twaK29Ioy5NGk2oLJrfKwaKZVeBDl7TzIxVX0wW3HQlCA=="], + + "@distilled.cloud/cloudflare-rolldown-plugin": ["@distilled.cloud/cloudflare-rolldown-plugin@0.10.1", "", { "dependencies": { "@cloudflare/unenv-preset": "^2.16.0", "magic-string": "^0.30.21", "unenv": "^2.0.0-rc.24" }, "peerDependencies": { "rolldown": "^1.0.1" }, "optionalPeers": ["rolldown"] }, "sha512-nEmEmUgbVkr9AH95/rq3cnWXbovBhXRAKW5/Ae1GwFndXBcdNy2Mx7w+AWUGuA3NSkDg6Q4W4EylPtJ3sduPqg=="], + + "@distilled.cloud/cloudflare-runtime": ["@distilled.cloud/cloudflare-runtime@0.10.1", "", { "dependencies": { "@alchemy.run/node-utils": "^0.0.4", "capnweb": "^0.7.0", "chokidar": "^4.0.1", "workerd": "1.20260526.1", "xdg-app-paths": "^8.3.0" }, "peerDependencies": { "@distilled.cloud/cloudflare": "^0.22.0", "@effect/platform-bun": ">=4.0.0-beta.66 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.66 || >=4.0.0", "effect": ">=4.0.0-beta.66 || >=4.0.0" }, "optionalPeers": ["@effect/platform-bun", "@effect/platform-node"] }, "sha512-KxVqZo8Kld4krux9yf4doSIwBlNdgCLY2ivIKBXv7vo3pY3pH3fcmCXE/+rGjD2iGqPU7eyEp4DdmKcAJAqhNw=="], + + "@distilled.cloud/cloudflare-vite-plugin": ["@distilled.cloud/cloudflare-vite-plugin@0.10.1", "", { "dependencies": { "@distilled.cloud/cloudflare-rolldown-plugin": "0.10.1" }, "peerDependencies": { "@distilled.cloud/cloudflare": "^0.22.0", "@distilled.cloud/cloudflare-runtime": "0.10.1", "@effect/platform-bun": ">=4.0.0-beta.66 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.66 || >=4.0.0", "effect": ">=4.0.0-beta.66 || >=4.0.0", "vite": "^7.0.0 || ^8.0.0" }, "optionalPeers": ["@effect/platform-bun", "@effect/platform-node"] }, "sha512-g0F/BoxUy8fg4U/IkeHyauXSQ8YU9N6aBOhybn0WvtLBeOUrqHT7R3+jMQsdt2PGKhkHPT3TlxN9FPaAUn0YSg=="], + + "@distilled.cloud/core": ["@distilled.cloud/core@0.22.4", "", { "peerDependencies": { "effect": ">=4.0.0-beta.66 || >=4.0.0" } }, "sha512-OuyM6cfOzQO0+n05Mb8jHDNrFxBjw27Q+9KL/uWRknLfTa3B1KDzyYJlt14MOFubl4YywreteNxGzcoGNhFN2A=="], + + "@distilled.cloud/neon": ["@distilled.cloud/neon@0.22.4", "", { "dependencies": { "@distilled.cloud/core": "0.22.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.66 || >=4.0.0" } }, "sha512-dE6h0mNTi5CUalMLcXgJmolYeCakB9bYCUHo1w/ERzwbb9MsyKLYckvRQbK1mcwKmrBdzZJWC1kaG8ZfNYDfTg=="], + + "@distilled.cloud/planetscale": ["@distilled.cloud/planetscale@0.22.4", "", { "dependencies": { "@distilled.cloud/core": "0.22.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.66 || >=4.0.0" } }, "sha512-SrMlYTnKBtZ0p6Sug4RzsSHRUPxJtjHLUmANI7q1SGDPoWZLCCfRuDdVTFBVi9WBjxlmJtDDjpHCQoEwA8qeLQ=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], + + "@ec-ts/twoslash": ["@ec-ts/twoslash@1.0.0", "", { "dependencies": { "@ec-ts/vfs": "^1.0.0", "twoslash-protocol": "^0.3.6" }, "peerDependencies": { "typescript": "^5.5.0" } }, "sha512-Bp4MCmeDWVRkTkuytPTszan+QGUqeftiasOBadvxSfQpbXYiWsle/N0lMrG+S14bfS9iBzI8mEqdGgd3Fvr3jQ=="], + + "@ec-ts/twoslash-vue": ["@ec-ts/twoslash-vue@1.0.0", "", { "dependencies": { "@ec-ts/twoslash": "^1.0.0", "@vue/language-core": "^3.2.5", "twoslash-protocol": "^0.3.6" }, "peerDependencies": { "typescript": "^5.5.0" } }, "sha512-3bB0/1DPPLHjS0eok2Cne7xQjCQVU5AWScEBGYTqsKuTCTukDUMxUiYI54rDPQME3OLnpysiORi0FQKLA96pAg=="], + + "@ec-ts/vfs": ["@ec-ts/vfs@1.0.0", "", { "peerDependencies": { "typescript": "^5.5.0" } }, "sha512-GQYRPMp58p9ak+TOwSLB/HZ+iknFyTRjqMTdXDkitNN3h5yjHuO+yeeO+/cuXiv7dyXGLYs4HaYrFgRKImp8yg=="], + + "@effect/language-service": ["@effect/language-service@0.77.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-QP2bri8DdcK7Eo+SqFS2yNeD0Ch9kKHYxq2jeE9CaPpBknevCNFb3+hT6qSsPt2P6yOkhNP83KMy5Uk7DGBXlg=="], + + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.74", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.74" }, "peerDependencies": { "effect": "^4.0.0-beta.74" } }, "sha512-HPozsTKom3v8uGITYX+blua0wVSk/I7ak802FoX13t4zWhHfprT8I/A8VBO4SxmcDYb/izABtHBXBgEmp612MA=="], + + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.74", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.74", "mime": "^4.1.0", "undici": "^8.2.0" }, "peerDependencies": { "effect": "^4.0.0-beta.74", "ioredis": "^5.7.0" } }, "sha512-/W16mKqxvhWINLjufzc0log1sl57exXQfwd+em398/zKCbmU3S7snXTDMN6w0ju2TtgK35qrsoGBXEochij6Sg=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.74", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.74" } }, "sha512-C6C2hXixNcZXLaFF2u7B/FtOsqpdY7luaPuiGFBJza0P7EnYDkwaT3kB6lv7l/qctmkADc24qOsSCWIKRbC4jg=="], + + "@effect/sql-pg": ["@effect/sql-pg@4.0.0-beta.74", "", { "dependencies": { "pg": "^8.20.0", "pg-connection-string": "2.12.0", "pg-cursor": "^2.19.0", "pg-pool": "^3.13.0", "pg-types": "^4.1.0" }, "peerDependencies": { "effect": "^4.0.0-beta.74" } }, "sha512-F1keL+vtp+d2HG7LQQ/WhJx4ezCrNy29T0HLbGPPvTeJTIi5/8fe2frh4WF55p6K3oi8jhkfe3fSH2Lcx0RjRw=="], + + "@effect/vitest": ["@effect/vitest@4.0.0-beta.74", "", { "peerDependencies": { "effect": "^4.0.0-beta.74", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-+SFcjtbboJdWA+cP7JFW7Gp1PL7vj7uqvBnh9xY7JFU8VMsrHN5mblsqE7l7+ZFGpJvBGLgjGrhfe8QZ7f5vgg=="], + + "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], + + "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], + + "@emmetio/css-parser": ["@emmetio/css-parser@0.4.1", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ=="], + + "@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="], + + "@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="], + + "@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="], + + "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], + + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], + + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], + + "@expressive-code/core": ["@expressive-code/core@0.41.7", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-ck92uZYZ9Wba2zxkiZLsZGi9N54pMSAVdrI9uW3Oo9AtLglD5RmrdTwbYPCT2S/jC36JGB2i+pnQtBm/Ib2+dg=="], + + "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7" } }, "sha512-diKtxjQw/979cTglRFaMCY/sR6hWF0kSMg8jsKLXaZBSfGS0I/Hoe7Qds3vVEgeoW+GHHQzMcwvgx/MOIXhrTA=="], + + "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "shiki": "^3.2.2" } }, "sha512-DL605bLrUOgqTdZ0Ot5MlTaWzppRkzzqzeGEu7ODnHF39IkEBbFdsC7pbl3LbUQ1DFtnfx6rD54k/cdofbW6KQ=="], + + "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7" } }, "sha512-Ewpwuc5t6eFdZmWlFyeuy3e1PTQC0jFvw2Q+2bpcWXbOZhPLsT7+h8lsSIJxb5mS7wZko7cKyQ2RLYDyK6Fpmw=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + + "@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.7.2", "", {}, "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA=="], + + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@iconify-json/logos": ["@iconify-json/logos@1.2.11", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-fOo4pGEatuyuCFNL+cwquYMa2Im0oJHRHV7lt/Qqs5Ode/lPImHCQcfTtPzZj7qYMPb/h8YHN3TG54uEowrjNQ=="], + + "@iconify-json/lucide": ["@iconify-json/lucide@1.2.110", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-rLeHqnZZBxZbprbVwf6uY7HB5GkGVgvT9VujhjvaUEqFDLKZON6zR8K1f8uD1brBwf5TJ0TIvvW8mz5u2XJU+w=="], + + "@iconify/react": ["@iconify/react@6.0.2", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-SMmC2sactfpJD427WJEDN6PMyznTFMhByK9yLW0gOTtnjzzbsi/Ke/XqsumsavFPwNiXs8jSiYeZTmLCLwO+Fg=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], + + "@jsdevtools/ez-spawn": ["@jsdevtools/ez-spawn@3.0.4", "", { "dependencies": { "call-me-maybe": "^1.0.1", "cross-spawn": "^7.0.3", "string-argv": "^0.3.1", "type-detect": "^4.0.8" } }, "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA=="], + + "@libsql/client": ["@libsql/client@0.17.3", "", { "dependencies": { "@libsql/core": "^0.17.3", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-HXk9wiAoJbKFbyBH4O+aEhN6ir5ERXuXvwE5OD2eR4/5RUa3Pw/8L9zrnVdU+iNJitRvisPWaIwmhkO3bH7giA=="], + + "@libsql/core": ["@libsql/core@0.17.3", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-2UjK1i7JBkMduJo4WdvvBxMMvVJ31pArBZNONyz/GCJJAH+1UHat2X6vn10S/WpY5fKzIT98WqYFl2vzWRLOfg=="], + + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="], + + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.29", "", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="], + + "@libsql/hrana-client": ["@libsql/hrana-client@0.10.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5" } }, "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw=="], + + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], + + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ=="], + + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg=="], + + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w=="], + + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg=="], + + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg=="], + + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w=="], + + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="], + + "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="], + + "@monorepo-multi-stack/backend": ["@monorepo-multi-stack/backend@workspace:examples/monorepo-multi-stack/backend"], + + "@monorepo-multi-stack/frontend": ["@monorepo-multi-stack/frontend@workspace:examples/monorepo-multi-stack/frontend"], + + "@monorepo-single-stack/backend": ["@monorepo-single-stack/backend@workspace:examples/monorepo-single-stack/backend"], + + "@monorepo-single-stack/frontend": ["@monorepo-single-stack/frontend@workspace:examples/monorepo-single-stack/frontend"], + + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + + "@nodable/entities": ["@nodable/entities@2.1.1", "", {}, "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@nothing-but/utils": ["@nothing-but/utils@0.17.0", "", {}, "sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ=="], + + "@octokit/action": ["@octokit/action@6.1.0", "", { "dependencies": { "@octokit/auth-action": "^4.0.0", "@octokit/core": "^5.0.0", "@octokit/plugin-paginate-rest": "^9.0.0", "@octokit/plugin-rest-endpoint-methods": "^10.0.0", "@octokit/types": "^12.0.0", "undici": "^6.0.0" } }, "sha512-lo+nHx8kAV86bxvOVOI3vFjX3gXPd/L7guAUbvs3pUvnR2KC+R7yjBkA1uACt4gYhs4LcWP3AXSGQzsbeN2XXw=="], + + "@octokit/auth-action": ["@octokit/auth-action@4.1.0", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/types": "^13.0.0" } }, "sha512-m+3t7K46IYyMk7Bl6/lF4Rv09GqDZjYmNg8IWycJ2Fa3YE3DE7vQcV6G2hUPmR9NDqenefNJwVtlisMjzymPiQ=="], + + "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + + "@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + + "@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="], + + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="], + + "@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], + + "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="], + + "@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + + "@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="], + + "@oozcitak/infra": ["@oozcitak/infra@2.0.2", "", { "dependencies": { "@oozcitak/util": "^10.0.0" } }, "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA=="], + + "@oozcitak/url": ["@oozcitak/url@3.0.0", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0" } }, "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ=="], + + "@oozcitak/util": ["@oozcitak/util@10.0.0", "", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.52.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.52.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.52.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.52.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.52.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.52.0", "", { "os": "none", "cpu": "arm64" }, "sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.52.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.52.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.67.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.67.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.67.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.67.0", "", { "os": "none", "cpu": "arm64" }, "sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.67.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.67.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ=="], + + "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="], + + "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="], + + "@pagefind/default-ui": ["@pagefind/default-ui@1.5.2", "", {}, "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg=="], + + "@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.5.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA=="], + + "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw=="], + + "@pagefind/linux-x64": ["@pagefind/linux-x64@1.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA=="], + + "@pagefind/windows-arm64": ["@pagefind/windows-arm64@1.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g=="], + + "@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-wasm": ["@parcel/watcher-wasm@2.5.6", "", { "dependencies": { "is-glob": "^4.0.3", "napi-wasm": "^1.1.0", "picomatch": "^4.0.3" } }, "sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@prettier/sync": ["@prettier/sync@0.6.1", "", { "dependencies": { "make-synchronized": "^0.8.0" }, "peerDependencies": { "prettier": "*" } }, "sha512-yF9G8vK/LYUTF3Cijd7VC9La3b20F20/J/fgoR4H0B8JGOWnZVZX6+I6+vODPosjmMcpdlUV+gUqJQZp3kLOcw=="], + + "@puppeteer/browsers": ["@puppeteer/browsers@2.2.4", "", { "dependencies": { "debug": "^4.3.5", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.4.0", "semver": "^7.6.2", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-BdG2qiI1dn89OTUUsx2GZSpUzW+DRffR1wlMJyKxVHYrhnKoELSDxDd+2XImUkuWPEKk76H5FcM/gPFrEK1Tfw=="], + + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], + + "@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="], + + "@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="], + + "@resvg/resvg-js-android-arm64": ["@resvg/resvg-js-android-arm64@2.6.2", "", { "os": "android", "cpu": "arm64" }, "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ=="], + + "@resvg/resvg-js-darwin-arm64": ["@resvg/resvg-js-darwin-arm64@2.6.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A=="], + + "@resvg/resvg-js-darwin-x64": ["@resvg/resvg-js-darwin-x64@2.6.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw=="], + + "@resvg/resvg-js-linux-arm-gnueabihf": ["@resvg/resvg-js-linux-arm-gnueabihf@2.6.2", "", { "os": "linux", "cpu": "arm" }, "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw=="], + + "@resvg/resvg-js-linux-arm64-gnu": ["@resvg/resvg-js-linux-arm64-gnu@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg=="], + + "@resvg/resvg-js-linux-arm64-musl": ["@resvg/resvg-js-linux-arm64-musl@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg=="], + + "@resvg/resvg-js-linux-x64-gnu": ["@resvg/resvg-js-linux-x64-gnu@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw=="], + + "@resvg/resvg-js-linux-x64-musl": ["@resvg/resvg-js-linux-x64-musl@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ=="], + + "@resvg/resvg-js-win32-arm64-msvc": ["@resvg/resvg-js-win32-arm64-msvc@2.6.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ=="], + + "@resvg/resvg-js-win32-ia32-msvc": ["@resvg/resvg-js-win32-ia32-msvc@2.6.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w=="], + + "@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.1", "", { "os": "none", "cpu": "arm64" }, "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.1", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw=="], + + "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.45", "", { "os": "win32", "cpu": "ia32" }, "sha512-wODcGzlfxqS6D7BR0srkJk3drPwXYLu7jPHN27ce2c4PUnVVmJnp9mJzUQGT4LpmHmmVdMZ+P6hKvyTGBzc1CA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@rollup/plugin-alias": ["@rollup/plugin-alias@6.0.0", "", { "peerDependencies": { "rollup": ">=4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g=="], + + "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg=="], + + "@rollup/plugin-inject": ["@rollup/plugin-inject@5.0.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg=="], + + "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="], + + "@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA=="], + + "@rollup/plugin-terser": ["@rollup/plugin-terser@1.0.0", "", { "dependencies": { "serialize-javascript": "^7.0.3", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="], + + "@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], + + "@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="], + + "@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@smithy/core": ["@smithy/core@3.24.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-c2G9QJ4xVZLwAkAf+WQESSSCkKbtt33ytje1klGvTcBn6cKuqV28E+62wbRPHwuTikkB3LQ7CBnNrayCoJur5A=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-3dA9TQ+ybRSZ/m0wnbZhiBy4Dezjgq1Ib/ZZrYTpJDBgpoLLU/SDzZc/g0x0MNAdOJe1wPcM+x2PBRmoOur+Sw=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-W7IPDXj8AZdyH5EWEXmOvN7ao8iN0JKJ0FNLpGcqj08HZc0MmqGcJnGgh3DfUdGYtzrPIEudxs+ovq/EWZgLjg=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA=="], + + "@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-2J8l+DoX3IIiP75X5SYkJ3mIgOkxW29MxOs7oPjbXLuInQ7UL6zLw2IJHbQ44+eKDBBhTjvt+GgwsTTNBGt8zA=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@solid-devtools/debugger": ["@solid-devtools/debugger@0.28.1", "", { "dependencies": { "@nothing-but/utils": "~0.17.0", "@solid-devtools/shared": "^0.20.0", "@solid-primitives/bounds": "^0.1.1", "@solid-primitives/event-listener": "^2.4.1", "@solid-primitives/keyboard": "^1.3.1", "@solid-primitives/rootless": "^1.5.1", "@solid-primitives/scheduled": "^1.5.1", "@solid-primitives/static-store": "^0.1.1", "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.9.0" } }, "sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg=="], + + "@solid-devtools/logger": ["@solid-devtools/logger@0.9.11", "", { "dependencies": { "@nothing-but/utils": "~0.17.0", "@solid-devtools/debugger": "^0.28.1", "@solid-devtools/shared": "^0.20.0", "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.9.0" } }, "sha512-THbiY1iQlieL6vdgJc4FIsLe7V8a57hod/Thm8zdKrTkWL88UPZjkBBfM+mVNGusd4OCnAN20tIFBhNnuT1Dew=="], + + "@solid-devtools/shared": ["@solid-devtools/shared@0.20.0", "", { "dependencies": { "@nothing-but/utils": "~0.17.0", "@solid-primitives/event-listener": "^2.4.1", "@solid-primitives/media": "^2.3.1", "@solid-primitives/refs": "^1.1.1", "@solid-primitives/rootless": "^1.5.1", "@solid-primitives/scheduled": "^1.5.1", "@solid-primitives/static-store": "^0.1.1", "@solid-primitives/styles": "^0.1.1", "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.9.0" } }, "sha512-o5TACmUOQsxpzpOKCjbQqGk8wL8PMi+frXG9WNu4Lh3PQVUB6hs95Kl/S8xc++zwcMguUKZJn8h5URUiMOca6Q=="], + + "@solid-primitives/bounds": ["@solid-primitives/bounds@0.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/resize-observer": "^2.1.5", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-JFym8zijMfWp1FaAmJlH3xMfenCuhjaUsoBn3kt9FtoWwLj+yt+EGYt+p3SkOKwF7h4gaGtZ5PIdSbSNVWkRmg=="], + + "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="], + + "@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sav+l+PL+74z3yaftVs7qd8c2SXkqzuxPOVibUe5wYMt+U5Hxp3V3XCPgBPN2I6cANjvoFtz0NiU8uHVLdi9FQ=="], + + "@solid-primitives/media": ["@solid-primitives/media@2.3.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-LX9fB5WDaK87FMDtUB1qokBOfT2et9Uobv/zZaKLH9caFSz4+P70MBKEIBHcZQy+9MV5M2XvGYLTbLskjkzMjA=="], + + "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="], + + "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="], + + "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], + + "@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g=="], + + "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ=="], + + "@solid-primitives/styles": ["@solid-primitives/styles@0.1.3", "", { "dependencies": { "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-7YdA21prMeCX+oOF/1RAn02+cGz/pG4dyPWtHBC2H8aZvnC7IfThBt80mP+TioejrdfE7Lc54Uh18f7Pig+gRQ=="], + + "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], + + "@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="], + + "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="], + + "@solidjs/start": ["@solidjs/start@2.0.0-alpha.2", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite-plugin-solid": "^2.11.9" }, "peerDependencies": { "vite": "^7" } }, "sha512-z56ATi3P07q8F5Io2I+RQrwjyWZtFZzpXN/J+8scf/gqrAW83LtgRkZFZjJaGH7i9WrHP+ep9F+ZiJ2gDHVBcw=="], + + "@solidjs/vite-plugin-nitro-2": ["@solidjs/vite-plugin-nitro-2@0.1.0", "", { "dependencies": { "nitropack": "^2.11.10" }, "peerDependencies": { "vite": "^7" } }, "sha512-gtT9GYhAdbfY2v3ISKYFXclxH/kK+mDhp5ENjiA1zJAfQq6XPWwIfIYm9MB73vfOp+MzVXWDHxYyUv+5pTpGTw=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], + + "@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.133.19", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw=="], + + "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], + + "@tanstack/react-router": ["@tanstack/react-router@1.170.9", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.7", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-rXXztp6GyXFwResQR0jdvkf52wy/sAQqze3OR08+8uNM7nHir1P46qBrwg2TL5EEtX+0WUho4BZ/nMpAgQVB6A=="], + + "@tanstack/react-start": ["@tanstack/react-start@1.168.16", "", { "dependencies": { "@tanstack/react-router": "1.170.9", "@tanstack/react-start-client": "1.168.6", "@tanstack/react-start-rsc": "0.1.15", "@tanstack/react-start-server": "1.167.11", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.5", "@tanstack/start-plugin-core": "1.171.8", "@tanstack/start-server-core": "1.169.6", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-ZkrGThxJG0SPg5V+VB4C+7KKx8kl1vo1lQY9FR2t6EqNKfZhl5N9qpkPOeyJNhse1pqe8Ihbv8LNL9c1/SohkQ=="], + + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.6", "", { "dependencies": { "@tanstack/react-router": "1.170.9", "@tanstack/router-core": "1.171.7", "@tanstack/start-client-core": "1.170.5" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-6G/ef89hx3yWfoHLKpDxDpgMsje889bh8tpy6SMeELrCW6ob8x0G+peoFpB5zhNVE03wCFXhImM6KkZFRwSyaQ=="], + + "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.15", "", { "dependencies": { "@tanstack/react-router": "1.170.9", "@tanstack/react-start-server": "1.167.11", "@tanstack/router-core": "1.171.7", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.5", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.8", "@tanstack/start-server-core": "1.169.6", "@tanstack/start-storage-context": "1.167.9", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-AdO+3OQicxFibsXEOIG2u2aRIAc1KIWMC98iiwaIUw/1ObP7uilZcYQynwdDeqsGKd0Ulr+tXZXebLdQwV4OeA=="], + + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.11", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-router": "1.170.9", "@tanstack/router-core": "1.171.7", "@tanstack/start-client-core": "1.170.5", "@tanstack/start-server-core": "1.169.6" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-7IOSbMJ7mvNbcI6/LAHsYbhR+oIxDfZlWSBau1E/l2tY2HSnKdtu+tj1yTY7zgwqEdn14IyGFSxpHe/eTFNB5g=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + + "@tanstack/router-core": ["@tanstack/router-core@1.171.7", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-AboyQD0OPIu0adJi6HeCupTU9Wx7zr4q4ceJYQdBL3mLH18m4M57XXdzJdtzOg/twl8DtWej0RGJ12ma8CV1iQ=="], + + "@tanstack/router-generator": ["@tanstack/router-generator@1.167.11", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.7", "@tanstack/router-utils": "1.162.1", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-Wnom12QTx0lreBQEL3zE2N88SDIT6xferScd46dnPCEYR8u+AkBBGRzlzZEpniIm+eF3byvIKBjYQgemdadoFg=="], + + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.12", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.7", "@tanstack/router-generator": "1.167.11", "@tanstack/router-utils": "1.162.1", "@tanstack/virtual-file-routes": "1.162.0", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.9", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-DqeJs+/LinTsTc6gIsloauHdCjePk6pmhpmcqrNWbNi8dS+QMj2UZ4HgU9JgOiYGznkd49i8BeQJFUxzASmr7Q=="], + + "@tanstack/router-utils": ["@tanstack/router-utils@1.162.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A=="], + + "@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="], + + "@tanstack/solid-router": ["@tanstack/solid-router@1.170.9", "", { "dependencies": { "@solid-devtools/logger": "^0.9.4", "@solid-primitives/refs": "^1.0.8", "@solidjs/meta": "^0.29.4", "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.7", "isbot": "^5.1.22" }, "peerDependencies": { "solid-js": "^1.9.10" } }, "sha512-qBGERBR0pL3nv4i7eMzEeV1ZLof64+7YgtOiCGvAw6Wh6XBDRDm2mGTnGoFZMJ8A1cmTGZ+/W4+Oi5CG1IXuyg=="], + + "@tanstack/solid-start": ["@tanstack/solid-start@1.168.16", "", { "dependencies": { "@tanstack/solid-router": "1.170.9", "@tanstack/solid-start-client": "1.168.6", "@tanstack/solid-start-server": "1.167.11", "@tanstack/start-client-core": "1.170.5", "@tanstack/start-plugin-core": "1.171.8", "@tanstack/start-server-core": "1.169.6", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "solid-js": ">=1.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-0v/m22301RYzUW+w/WD09JgMn/734+XzBpzshQtLrzQOvxFAuocJiCxpO1M1cs1CAA3mxz8bmoqm33YEY3S2dw=="], + + "@tanstack/solid-start-client": ["@tanstack/solid-start-client@1.168.6", "", { "dependencies": { "@tanstack/router-core": "1.171.7", "@tanstack/solid-router": "1.170.9", "@tanstack/start-client-core": "1.170.5" }, "peerDependencies": { "solid-js": ">=1.0.0" } }, "sha512-RbFCRuvB3AplxN0kfS9ZLb4g1ySNyMwGiNWDQp1tUsUlHGTTMIncqBoRZ8bKflrtf+tOw3NCiV0z3IMX5KrILg=="], + + "@tanstack/solid-start-server": ["@tanstack/solid-start-server@1.167.11", "", { "dependencies": { "@solidjs/meta": "^0.29.4", "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.7", "@tanstack/solid-router": "1.170.9", "@tanstack/start-client-core": "1.170.5", "@tanstack/start-server-core": "1.169.6" }, "peerDependencies": { "solid-js": "^1.0.0" } }, "sha512-kbUJQ4Sedyt5exsKliigSikF5JZ2qaUjWDWDV5QiexhN3/xlBBZzkj2P1qeYjA0DV8Z8bbbrmg2MxUUFG1TZ0Q=="], + + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.5", "", { "dependencies": { "@tanstack/router-core": "1.171.7", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.9", "seroval": "^1.5.4" } }, "sha512-rtL6JyEB7/zeylgOjwtFEtmTIwRUm4Mf4bEhp2yUVk8kB4zLlmYcpSAFbb/aIrLp/Yl9XS2dx3OxB7oIPUuplg=="], + + "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="], + + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.8", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.1", "@tanstack/router-core": "1.171.7", "@tanstack/router-generator": "1.167.11", "@tanstack/router-plugin": "1.168.12", "@tanstack/router-utils": "1.162.1", "@tanstack/start-client-core": "1.170.5", "@tanstack/start-server-core": "1.169.6", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.4", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-Eoe83LD3N7AQgsjb+uRq+DjUE82+e8liYqzfIVoZ03t4+XfHtTbMBB+f6XK2kUzTv4QBG7ZmZU959jWwQHfVTQ=="], + + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.6", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.7", "@tanstack/start-client-core": "1.170.5", "@tanstack/start-storage-context": "1.167.9", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.4" } }, "sha512-Qf+Uinnc5Ak79QGD6pJsiK0IfUlWkyJNy+h9vTwOtVLBvmq/fR7GjBuf5ymidI/7jK/YQWVuw2aaEE6MHBC+iw=="], + + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.9", "", { "dependencies": { "@tanstack/router-core": "1.171.7" } }, "sha512-RKxJA95HgZXu2lgTCzGi7pShtgz7vyvDTEPOJfJBBMHM2S7SX/WLYTHqfHcYvbOzREXOxEbDQ8Hvtt0Klq8VKg=="], + + "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], + + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + + "@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="], + + "@tsconfig/node24": ["@tsconfig/node24@24.0.4", "", {}, "sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@types/aws-lambda": ["@types/aws-lambda@8.10.161", "", {}, "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/micromatch": ["@types/micromatch@4.0.10", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], + + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], + + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + + "@types/picomatch": ["@types/picomatch@4.0.3", "", {}, "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ=="], + + "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.0", "@typescript-eslint/types": "^8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.0", "@typescript-eslint/tsconfig-utils": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="], + + "@typescript/analyze-trace": ["@typescript/analyze-trace@0.10.1", "", { "dependencies": { "chalk": "^4.1.2", "exit": "^0.1.2", "jsonparse": "^1.3.1", "jsonstream-next": "^3.0.0", "p-limit": "^3.1.0", "split2": "^3.2.2", "treeify": "^1.1.0", "yargs": "^16.2.0" }, "bin": { "analyze-trace": "bin/analyze-trace", "print-trace-types": "bin/print-trace-types", "simplify-trace-types": "bin/simplify-trace-types" } }, "sha512-RnlSOPh14QbopGCApgkSx5UBgGda5MX1cHqp2fsqfiDyCwGL/m1jaeB9fzu7didVS81LQqGZZuxFBcg8YU8EVw=="], + + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260527.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260527.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260527.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260527.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260527.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260527.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260527.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260527.2" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-piqkDwikVeizCFqA1lcwI5F4wOAtBdxuliWe77ApBNRyBPPvfCJB+u/HYi9/8t5nd0sWvFs6/qt/AzJ1CCoykQ=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260527.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3LqSu4DlxkEfeC/Z/29QMCJn5jjkDtXI7LYuxfmjdmAatS6umDKqm8J17fnP/7fyrZUMBTIYRwSDpChGV3G1ew=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260527.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-H4+sxE9qaBbLF83wMdWE0FsgfK0Pom+/O+/oxqyGzhVkDJlNt3vfpgQZMit48/Gm44AacGfBggJ9Dhbi3aeSFw=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260527.2", "", { "os": "linux", "cpu": "arm" }, "sha512-6I9Cv9ozwfS9zB9vRQDPIYseLX3artEO9jl3yVgLj4ishwlSF4cWAbIsjl5IztPaEgHv8coej/6tX1D0uaBzXg=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260527.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-BGUDMjC2Z3TTdZRkGGwhBLelkP5UYgO2rbep8aF4dS3fu7T5lFPPrnfS6EgqJgie+cF5Fsev7xEq8wWyBDM+lg=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260527.2", "", { "os": "linux", "cpu": "x64" }, "sha512-vpazOu+ozlxBo8U57YJMzsOPuxAV8H7fu36KJ8ea8At/D8pdGmOAy5TuB+9OBQV9JDe0OXJMy2kmbhOpmkTAmA=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260527.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-DBFnFE3V6AITkPO1K1VxXf3yEZKjU2FwtXlNwRqhzDu0rrL2SsJHOSrBDX+OacTxQFzZMxFcpiuhV8jHZALPEg=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260527.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1tBlErMvQgcMqqYwsx4tytupcjCJcOUXD3vBn1Wb/kAvus1FzWQAFE0fcKBvLfcqLQfTiiEwKKEtbLjGmakqqg=="], + + "@typescript/vfs": ["@typescript/vfs@1.6.1", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + + "@vercel/nft": ["@vercel/nft@1.10.2", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-w+WyX5Ulmj4dtTZrxaulqrjaLZHSbnPzx75SJsTNYmotKsqn1JlLnDJa+lz5hn90HJofhl/2MAtw0mCrgM3qYw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.7", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg=="], + + "@vitest/expect": ["@vitest/expect@4.1.7", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.7", "", { "dependencies": { "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.7", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw=="], + + "@vitest/runner": ["@vitest/runner@4.1.7", "", { "dependencies": { "@vitest/utils": "4.1.7", "pathe": "^2.0.3" } }, "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "@vitest/utils": "4.1.7", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw=="], + + "@vitest/spy": ["@vitest/spy@4.1.7", "", {}, "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q=="], + + "@vitest/ui": ["@vitest/ui@4.1.7", "", { "dependencies": { "@vitest/utils": "4.1.7", "fflate": "^0.8.2", "flatted": "^3.4.2", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "vitest": "4.1.7" } }, "sha512-TP6utB2yX6rsJNVRo2qAlsi48i1YwFTrLV2tnTtWqJaYX7m4lRCCLirZBjU6xC5m0RsPHr+L2+N+eIPhgEzFfw=="], + + "@vitest/utils": ["@vitest/utils@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw=="], + + "@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="], + + "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="], + + "@volar/language-server": ["@volar/language-server@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw=="], + + "@volar/language-service": ["@volar/language-service@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw=="], + + "@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="], + + "@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="], + + "@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="], + + "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="], + + "@vue-macros/common": ["@vue-macros/common@3.1.2", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng=="], + + "@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="], + + "@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@1.5.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.2", "@vue/babel-helper-vue-transform-on": "1.5.0", "@vue/babel-plugin-resolve-type": "1.5.0", "@vue/shared": "^3.5.18" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw=="], + + "@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.5.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.0", "@vue/compiler-sfc": "^3.5.18" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.6.0-beta.13", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.6.0-beta.13", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-axPx/FRs1Vg6XoVTrn+jIBcl46FAjnLH9/tZpNtzfUYShxY/nm8FpNaaLAKAeAqyDBsGGoljnjv+Jg9Mvl1fqA=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.6.0-beta.13", "", { "dependencies": { "@vue/compiler-core": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13" } }, "sha512-CHY4yZQ2Pw8w/YEu8H2CvEZ7upPupliOcsFa8wqFAbVbNvUn7boAWlPQcAGygmn3rPJipwq+9DfSgUeWPumlCw=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.6.0-beta.13", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/compiler-core": "3.6.0-beta.13", "@vue/compiler-dom": "3.6.0-beta.13", "@vue/compiler-ssr": "3.6.0-beta.13", "@vue/compiler-vapor": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.14", "source-map-js": "^1.2.1" } }, "sha512-6mYwUHmpQEZzkeTaXjnyq//W3oJmGX2OgcBGFjP121ItbKYwY00C/Q2o9FWG7FojofbUa3PuMfF+FAQVI0Buow=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.6.0-beta.13", "", { "dependencies": { "@vue/compiler-dom": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13" } }, "sha512-MEQIxOUuoXrlkc10Zm6Qzu2eHB+Gnhzf0Z30aptfRNbmxyswj8MTIcpcQk6KsHuCtcVQXiQ+EFl3EauZBfp2dg=="], + + "@vue/compiler-vapor": ["@vue/compiler-vapor@3.6.0-beta.13", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/compiler-dom": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-n70deyGK3xUpo/xxsAJObfGEYubfbPfpMNIBUnisiVx8seCZ+qJ9gCLIJUAwiAiZWTLRVl5nblzhjikHmbx3Uw=="], + + "@vue/devtools-api": ["@vue/devtools-api@8.1.2", "", { "dependencies": { "@vue/devtools-kit": "^8.1.2" } }, "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg=="], + + "@vue/devtools-core": ["@vue/devtools-core@8.1.2", "", { "dependencies": { "@vue/devtools-kit": "^8.1.2", "@vue/devtools-shared": "^8.1.2" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-ZGGyaSBP4/+bN2Nd9ZHNYAVDRIzMw1rv2RyXWtyZlo6mQal+IDmTvKY4V+DjAEBhaXt30mHmsgYp1yXJ/2tIWg=="], + + "@vue/devtools-kit": ["@vue/devtools-kit@8.1.2", "", { "dependencies": { "@vue/devtools-shared": "^8.1.2", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ=="], + + "@vue/devtools-shared": ["@vue/devtools-shared@8.1.2", "", {}, "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw=="], + + "@vue/language-core": ["@vue/language-core@3.3.2", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.2.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.4" } }, "sha512-CLwjSfHlPLhjd2qhuS3tTFtnOIWHXAM5u4X1DxmzlQ8j5bmOYlKCsSusOP7jCRJnlVg0mCTQtHU3vwFvopZGoQ=="], + + "@vue/reactivity": ["@vue/reactivity@3.6.0-beta.13", "", { "dependencies": { "@vue/shared": "3.6.0-beta.13" } }, "sha512-iR/lv+EQmWgxP1k+Gc0HXFa9jv4EZEoAGzknl69dlUt3J6lOps8aymEdRS5gbrIjNPsZGwzSBfeScwv042KmMw=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.6.0-beta.13", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13" } }, "sha512-ejzb81LvwHbGxhswSPKu55PeVL4/Qud8yX6fKhqQrmOcGMdI7y6kRUHPVS9WeOkI+1qhWd/ApPKigdDqTDKsRA=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.6.0-beta.13", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.13", "@vue/runtime-core": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13", "csstype": "^3.2.3" } }, "sha512-eKCSL9IvjMzhmTzNZx710OuuhpauN80RdOOdu3aLZUPuXAk2S32vmGFU5dcEFx8jF7bQAJlVLbM3BUGCYODfuw=="], + + "@vue/runtime-vapor": ["@vue/runtime-vapor@3.6.0-beta.13", "", { "dependencies": { "@vue/reactivity": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13" }, "peerDependencies": { "@vue/runtime-dom": "3.6.0-beta.13" } }, "sha512-25bjquxxF7BRs7dUDuNcmPnLusOw60qDx55fo21ZpgQ4IhGhPengbENiXZp9N3gIxr7odzixZEVffg5OFEnwlg=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.6.0-beta.13", "", { "dependencies": { "@vue/compiler-ssr": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13" }, "peerDependencies": { "vue": "3.6.0-beta.13" } }, "sha512-x5Gjo+ad8PQMVTyCf+2yoHPXaCm2eeItAx0BVQMkRAxjpaA9P92fdczSVVUNp/QCoAFv0TbChgtWypRBoeMqXA=="], + + "@vue/shared": ["@vue/shared@3.6.0-beta.13", "", {}, "sha512-OUO/GCLRQk1DJmuuAU7QT7mGp7cY2B5Ajdsy1+T1xtdSm2rCsPi2GrWK1EF/nsnFM2t+RNxCjEt3oT+Wm5P2fA=="], + + "@vue/tsconfig": ["@vue/tsconfig@0.9.1", "", { "peerDependencies": { "typescript": ">= 5.8", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w=="], + + "abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], + + "alchemy": ["alchemy@workspace:packages/alchemy"], + + "alien-signals": ["alien-signals@3.2.1", "", {}, "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g=="], + + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], + + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "ansis": ["ansis@4.3.0", "", {}, "sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], + + "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "arkregex": ["arkregex@0.0.4", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-biS/FkvSwQq59TZ453piUp8bxMui11pgOMV9WHAnli1F8o0ayNCZzUwQadL/bGIUic5TkS/QlPcyMuI8ZIwedQ=="], + + "arktype": ["arktype@2.1.28", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.4" } }, "sha512-LVZqXl2zWRpNFnbITrtFmqeqNkPPo+KemuzbGSY6jvJwCb4v8NsDzrWOLHnQgWl26TkJeWWcUNUeBpq2Mst1/Q=="], + + "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], + + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + + "ast-walker-scope": ["ast-walker-scope@0.9.0", "", { "dependencies": { "@babel/parser": "^7.29.2", "@babel/types": "^7.29.0", "ast-kit": "^2.2.0" } }, "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "astro": ["astro@5.18.2", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.6", "@astrojs/markdown-remark": "6.3.11", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.27.3", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-TnFwLnAXty5MXKPDGuKXqK4AMBXG+FH6RUdK7Oyc3gyfNoFIthT+4eRbzOK43bdRlLaZuxgciDSjgtggZ3OtGQ=="], + + "astro-broken-links-checker": ["astro-broken-links-checker@1.1.0", "", { "dependencies": { "fast-glob": "^3.3.3", "node-html-parser": "^7.0.1", "p-limit": "^7.2.0" } }, "sha512-TYfDUZl0iYq1dVZO+R0appTBkDmSMsOrafgWJMGqoD4xVd74imTRdzekE6tA8+6AIi+/GXMGlr/im0unLD1LRg=="], + + "astro-expressive-code": ["astro-expressive-code@0.41.7", "", { "dependencies": { "rehype-expressive-code": "^0.41.7" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-hUpogGc6DdAd+I7pPXsctyYPRBJDK7Q7d06s4cyP0Vz3OcbziP3FNzN0jZci1BpCvLn9675DvS7B9ctKKX64JQ=="], + + "astro-remote": ["astro-remote@0.3.4", "", { "dependencies": { "entities": "^4.5.0", "marked": "^12.0.0", "marked-footnote": "^1.2.2", "marked-smartypants": "^1.1.6", "ultrahtml": "^1.5.3" } }, "sha512-jL5skNQLA0YBc1R3bVGXyHew3FqGqsT7AgLzWAVeTLzFkwVMUYvs4/lKJSmS7ygcF1GnHnoKG6++8GL9VtWwGQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-sema": ["async-sema@3.1.1", "", {}, "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="], + + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + + "aws-ec2-example": ["aws-ec2-example@workspace:examples/aws-ec2"], + + "aws-ecs-example": ["aws-ecs-example@workspace:examples/aws-ecs"], + + "aws-eks-example": ["aws-eks-example@workspace:examples/aws-eks"], + + "aws-lambda": ["aws-lambda@workspace:examples/aws-lambda"], + + "aws-lambda-httpapi": ["aws-lambda-httpapi@workspace:examples/aws-lambda-httpapi"], + + "aws-lambda-rpc": ["aws-lambda-rpc@workspace:examples/aws-lambda-rpc"], + + "aws-rds-example": ["aws-rds-example@workspace:examples/aws-rds"], + + "aws-rest-api": ["aws-rest-api@workspace:examples/aws-rest-api"], + + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + + "aws-static-site-example": ["aws-static-site-example@workspace:examples/aws-static-site"], + + "aws-vite-example": ["aws-vite-example@workspace:examples/aws-vite"], + + "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], + + "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], + + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "bare-events": ["bare-events@2.8.3", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw=="], + + "bare-fs": ["bare-fs@4.7.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="], + + "bare-os": ["bare-os@3.9.1", "", {}, "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ=="], + + "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], + + "bare-stream": ["bare-stream@2.13.1", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow=="], + + "bare-url": ["bare-url@2.4.3", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ=="], + + "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], + + "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="], + + "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], + + "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], + + "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], + + "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + + "better-auth": ["better-auth@1.6.12", "", { "dependencies": { "@better-auth/core": "1.6.12", "@better-auth/drizzle-adapter": "1.6.12", "@better-auth/kysely-adapter": "1.6.12", "@better-auth/memory-adapter": "1.6.12", "@better-auth/mongo-adapter": "1.6.12", "@better-auth/prisma-adapter": "1.6.12", "@better-auth/telemetry": "1.6.12", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-vJG8hB+zcayZEJgcWGTzP2XODZuf/WKViOtam+uhhQ9879yc7fDWAV9O4jSs+R28noSXIAaB3zhIMN3DaDO3cA=="], + + "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="], + + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], + + "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], + + "capnweb": ["capnweb@0.6.1", "", {}, "sha512-fmhV26QPd1ewf5R74h55oVZnGwIcSaRMzbfLQUy8+zOBjuTmT3KXoT8wxHvnp1m9Ht9BoUUS5ZwNLoVLfQTyBg=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "changelogen": ["changelogen@0.5.7", "", { "dependencies": { "c12": "^1.11.2", "colorette": "^2.0.20", "consola": "^3.2.3", "convert-gitmoji": "^0.1.5", "mri": "^1.2.0", "node-fetch-native": "^1.6.4", "ofetch": "^1.3.4", "open": "^10.1.0", "pathe": "^1.1.2", "pkg-types": "^1.2.0", "scule": "^1.3.0", "semver": "^7.6.3", "std-env": "^3.7.0", "yaml": "^2.5.1" }, "bin": { "changelogen": "dist/cli.mjs" } }, "sha512-cTZXBcJMl3pudE40WENOakXkcVtrbBpbkmSkM20NdRiUqa4+VYRdXdEsgQ0BNQ6JBE2YymTNWtPKVF7UCTN5+g=="], + + "changelogithub": ["changelogithub@13.16.1", "", { "dependencies": { "ansis": "^4.2.0", "c12": "^3.3.1", "cac": "^6.7.14", "changelogen": "0.5.7", "convert-gitmoji": "^0.1.5", "execa": "^9.6.0", "ofetch": "^1.4.1", "semver": "^7.7.3", "tinyglobby": "^0.2.15" }, "bin": { "changelogithub": "cli.mjs" } }, "sha512-h4etOmEM/wtqBWKPbnHoqv2C8moRCCEGTckwwWpvRgr/t1tY0MbyjQbZKy1ETQ7gn1UTQMJkCSRQ4KxiQ+HfSQ=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], + + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], + + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "cloudflare-dev": ["cloudflare-dev@workspace:examples/cloudflare-dev"], + + "cloudflare-email": ["cloudflare-email@workspace:examples/cloudflare-email"], + + "cloudflare-git-artifacts": ["cloudflare-git-artifacts@workspace:examples/cloudflare-git-artifacts"], + + "cloudflare-neon-drizzle": ["cloudflare-neon-drizzle@workspace:examples/cloudflare-neon-drizzle"], + + "cloudflare-planetscale-mysql-drizzle": ["cloudflare-planetscale-mysql-drizzle@workspace:examples/cloudflare-planetscale-mysql-drizzle"], + + "cloudflare-planetscale-postgres-drizzle": ["cloudflare-planetscale-postgres-drizzle@workspace:examples/cloudflare-planetscale-postgres-drizzle"], + + "cloudflare-secrets-store": ["cloudflare-secrets-store@workspace:examples/cloudflare-secrets-store"], + + "cloudflare-solid-ssr": ["cloudflare-solid-ssr@workspace:examples/cloudflare-solidjs-ssr"], + + "cloudflare-tanstack-example": ["cloudflare-tanstack-example@workspace:examples/cloudflare-tanstack"], + + "cloudflare-tanstack-start-solid-example": ["cloudflare-tanstack-start-solid-example@workspace:examples/cloudflare-tanstack-start-solid"], + + "cloudflare-vite-example": ["cloudflare-vite-example@workspace:examples/cloudflare-static-site"], + + "cloudflare-vue": ["cloudflare-vue@workspace:examples/cloudflare-vue"], + + "cloudflare-worker": ["cloudflare-worker@workspace:examples/cloudflare-worker"], + + "cloudflare-worker-async": ["cloudflare-worker-async@workspace:examples/cloudflare-worker-async"], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], + + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + + "compatx": ["compatx@0.2.0", "", {}, "sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA=="], + + "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], + + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "convert-gitmoji": ["convert-gitmoji@0.1.5", "", {}, "sha512-4wqOafJdk2tqZC++cjcbGcaJ13BZ3kwldf06PTiAQRAB76Z1KJwZNL1SaRZMi2w1FM9RYTgZ6QErS8NUl/GBmQ=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + + "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="], + + "croner": ["croner@10.0.1", "", {}, "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + + "css-background-parser": ["css-background-parser@0.1.0", "", {}, "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="], + + "css-box-shadow": ["css-box-shadow@1.0.0-3", "", {}, "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="], + + "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], + + "css-gradient-parser": ["css-gradient-parser@0.0.17", "", {}, "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg=="], + + "css-js-gen": ["css-js-gen@1.1.0", "", {}, "sha512-CuPSTe6EwlrDHcMFOfcO3SwVqclVKLnqSbTGGYzfFt8z2NenGeupp47Z3wx+yhQ2pdozb3R8hc+nyECBTykooQ=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="], + + "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "cytoscape": ["cytoscape@3.33.4", "", {}, "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + + "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], + + "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], + + "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "devtools-protocol": ["devtools-protocol@0.0.1299070", "", {}, "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "dompurify": ["dompurify@3.4.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + + "drizzle-kit": ["drizzle-kit@1.0.0-rc.1", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-eDvXzRhke7OwvmN7AciGOU1E2y17MKNhghGciyw1RbmmkuD/2KDXLn3rFRZcDBmfj6CQSEnyvbU+7Fqrn2JQyA=="], + + "drizzle-orm": ["drizzle-orm@1.0.0-rc.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-jGCqAgxpz+OSHP2jQGooUHBxnFMTYl0TTRSfULBl52VNf7CtyNRnazUi+VdbSxvJrDP2lnIsmUh5O+HhKeSJCg=="], + + "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], + + "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], + + "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.364", "", {}, "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw=="], + + "emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "emoji-regex-xs": ["emoji-regex-xs@2.0.1", "", {}, "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g=="], + + "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + + "es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + + "example-basic": ["example-basic@workspace:examples/cloudflare-solidstart"], + + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + + "exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="], + + "expressive-code-twoslash": ["expressive-code-twoslash@0.6.1", "", { "dependencies": { "@ec-ts/twoslash": "^1.0.0", "@ec-ts/twoslash-vue": "^1.0.0", "@typescript-eslint/parser": "^8.56.1", "css-js-gen": "^1.1.0", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", "mdast-util-to-hast": "^13.2.1", "twoslash-eslint": "^0.3.6" }, "peerDependencies": { "@expressive-code/core": "^0.41.7", "expressive-code": "^0.41.7", "typescript": "^5.5.0" } }, "sha512-aEeKBgqg6cF4rQPxT5BHNbRffwsA66zWkNT/Y7CcUIE8eHW6KJHPCnZrGCLxvQ5xRd+qWj7ZXq84gT2zd2ECbg=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], + + "fast-xml-parser": ["fast-xml-parser@5.8.0", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", "strnum": "^2.3.0", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetchdts": ["fetchdts@0.1.7", "", {}, "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA=="], + + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="], + + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], + + "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], + + "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + + "giget": ["giget@3.2.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@16.2.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "is-path-inside": "^4.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.4.0" } }, "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q=="], + + "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="], + + "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], + + "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="], + + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], + + "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], + + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], + + "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "hex-rgb": ["hex-rgb@4.3.0", "", {}, "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], + + "html-to-image": ["html-to-image@1.11.13", "", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "http-shutdown": ["http-shutdown@1.2.2", "", {}, "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "httpxy": ["httpxy@0.5.3", "", {}, "sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], + + "ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "ioredis": ["ioredis@5.11.0", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg=="], + + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + + "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + + "isbot": ["isbot@5.1.40", "", {}, "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ=="], + + "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json-with-bigint": ["json-with-bigint@3.5.8", "", {}, "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "jsonstream-next": ["jsonstream-next@3.0.0", "", { "dependencies": { "jsonparse": "^1.2.0", "through2": "^4.0.2" }, "bin": { "jsonstream-next": "bin.js" } }, "sha512-aAi6oPhdt7BKyQn1SrIIGZBt0ukKuOUE1qV6kJ3GgioSOYzsRc8z9Hfr1BVmacA/jLe9nARfmgMGgn68BqIAgg=="], + + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + + "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="], + + "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + + "kysely": ["kysely@0.29.2", "", {}, "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg=="], + + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "libsodium": ["libsodium@0.8.4", "", {}, "sha512-lMcYaRi0zcs7tarATsQUYC7rstliIXZuoq0c6zXSgNtSNtdvBgkSegjWhpMJAXzKX3SUSwIp7+zEsob+j3LuRw=="], + + "libsodium-wrappers": ["libsodium-wrappers@0.8.4", "", { "dependencies": { "libsodium": "^0.8.0" } }, "sha512-mu8aAWucZjTB5O/BtGXtW4e1agy7uHxNYG7zPthmmD1jU43LCDmSWZLN4JhflbdPXj3yDO4lxM1O9hLDgIOXDw=="], + + "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], + + "listhen": ["listhen@1.10.0", "", { "dependencies": { "@parcel/watcher": "^2.5.6", "@parcel/watcher-wasm": "^2.5.6", "citty": "^0.2.2", "consola": "^3.4.2", "crossws": ">=0.2.0 <0.5.0", "defu": "^6.1.7", "get-port-please": "^3.2.0", "h3": "^1.15.11", "http-shutdown": "^1.2.2", "jiti": "^2.6.1", "mlly": "^1.8.2", "node-forge": "^1.4.0", "pathe": "^2.0.3", "std-env": "^4.1.0", "tinyclip": "^0.1.12", "ufo": "^1.6.4", "untun": "^0.1.3", "uqr": "^0.1.3" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-kfz4C0OrC6IpaVMtYDJtf6PFjurxe9NBBoDAh/o2p587INryFOO4DQ9OetbCdDrWFt1m1CJKvYrzkGsuPHw8nQ=="], + + "local-pkg": ["local-pkg@1.2.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], + + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="], + + "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], + + "make-synchronized": ["make-synchronized@0.8.0", "", {}, "sha512-DZu4lwc0ffoFz581BSQa/BJl+1ZqIkoRQ+VejMlH0VrP4E86StAODnZujZ4sepumQj8rcP7wUnUBGM8Gu+zKUA=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + + "marked-footnote": ["marked-footnote@1.4.0", "", { "peerDependencies": { "marked": ">=7.0.0" } }, "sha512-fZTxAhI1TcLEs5UOjCfYfTHpyKGaWQevbxaGTEA68B51l7i87SctPFtHETYqPkEN0ka5opvy4Dy1l/yXVC+hmg=="], + + "marked-plaintify": ["marked-plaintify@1.1.1", "", { "peerDependencies": { "marked": ">=13.0.0" } }, "sha512-r3kMKArhfo2H3lD4ctFq/OJTzM0uNvXHh7FBTI1hMDpf4Ac1djjtq4g8NfTBWMxWLmaEz3KL1jCkLygik3gExA=="], + + "marked-smartypants": ["marked-smartypants@1.1.12", "", { "dependencies": { "smartypants": "^0.2.2" }, "peerDependencies": { "marked": ">=4 <19" } }, "sha512-Z0QL2GpihbSeG5aaCrQxMEoqvngMftF/gq1SrdlCnbecUSrX3HYgPtCZzCW+OyNe2ideQqaFdxfGryqQX1MBDA=="], + + "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], + + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], + + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="], + + "mermaid-isomorphic": ["mermaid-isomorphic@3.1.0", "", { "dependencies": { "@fortawesome/fontawesome-free": "^6.0.0", "katex": "^0.16.0", "mermaid": "^11.0.0" }, "peerDependencies": { "playwright": "1" }, "optionalPeers": ["playwright"] }, "sha512-mzrvfEVjnJIkJlEqxp3eMuR1wS0TeLCH1VK5E/T5yzWaBwI3JqjJuw70yUIThSCDJ5bRs6O3rgfp00oBAbvSeQ=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], + + "miniflare": ["miniflare@4.20260526.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.8", "workerd": "1.20260526.1", "ws": "8.20.1", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JYQ7jPZZWoaaj9jWHb8Ucp6Cu2SbDVqIsAJhumqdzzLkkfq0pYkDeino/sZfW1ixJWPjv/C44zjm9gVJC2izCA=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], + + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + + "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + + "mysql2": ["mysql2@3.22.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ=="], + + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], + + "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], + + "nitropack": ["nitropack@2.13.4", "", { "dependencies": { "@cloudflare/kv-asset-handler": "^0.4.2", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@vercel/nft": "^1.5.0", "archiver": "^7.0.1", "c12": "^3.3.4", "chokidar": "^5.0.0", "citty": "^0.2.2", "compatx": "^0.2.0", "confbox": "^0.2.4", "consola": "^3.4.2", "cookie-es": "^2.0.1", "croner": "^10.0.1", "crossws": "^0.3.5", "db0": "^0.3.4", "defu": "^6.1.7", "destr": "^2.0.5", "dot-prop": "^10.1.0", "esbuild": "^0.28.0", "escape-string-regexp": "^5.0.0", "etag": "^1.8.1", "exsolve": "^1.0.8", "globby": "^16.2.0", "gzip-size": "^7.0.0", "h3": "^1.15.11", "hookable": "^5.5.3", "httpxy": "^0.5.1", "ioredis": "^5.10.1", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "listhen": "^1.9.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mime": "^4.1.0", "mlly": "^1.8.2", "node-fetch-native": "^1.6.7", "node-mock-http": "^1.0.4", "ofetch": "^1.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.1", "pretty-bytes": "^7.1.0", "radix3": "^1.1.2", "rollup": "^4.60.2", "rollup-plugin-visualizer": "^7.0.1", "scule": "^1.3.0", "semver": "^7.7.4", "serve-placeholder": "^2.0.2", "serve-static": "^2.2.1", "source-map": "^0.7.6", "std-env": "^4.1.0", "ufo": "^1.6.4", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.5.0", "unenv": "2.0.0-rc.24", "unimport": "^6.2.0", "unplugin-utils": "^0.3.1", "unstorage": "^1.17.5", "untyped": "^2.0.0", "unwasm": "^0.5.3", "youch": "^4.1.1", "youch-core": "^0.3.3" }, "peerDependencies": { "xml2js": "^0.6.2" }, "optionalPeers": ["xml2js"], "bin": { "nitro": "dist/cli/index.mjs", "nitropack": "dist/cli/index.mjs" } }, "sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA=="], + + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + + "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], + + "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], + + "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], + + "nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="], + + "npm-run-all2": ["npm-run-all2@8.0.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="], + + "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], + + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "os-paths": ["os-paths@7.4.0", "", { "optionalDependencies": { "fsevents": "*" } }, "sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA=="], + + "oxfmt": ["oxfmt@0.52.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.52.0", "@oxfmt/binding-android-arm64": "0.52.0", "@oxfmt/binding-darwin-arm64": "0.52.0", "@oxfmt/binding-darwin-x64": "0.52.0", "@oxfmt/binding-freebsd-x64": "0.52.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.52.0", "@oxfmt/binding-linux-arm-musleabihf": "0.52.0", "@oxfmt/binding-linux-arm64-gnu": "0.52.0", "@oxfmt/binding-linux-arm64-musl": "0.52.0", "@oxfmt/binding-linux-ppc64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-gnu": "0.52.0", "@oxfmt/binding-linux-riscv64-musl": "0.52.0", "@oxfmt/binding-linux-s390x-gnu": "0.52.0", "@oxfmt/binding-linux-x64-gnu": "0.52.0", "@oxfmt/binding-linux-x64-musl": "0.52.0", "@oxfmt/binding-openharmony-arm64": "0.52.0", "@oxfmt/binding-win32-arm64-msvc": "0.52.0", "@oxfmt/binding-win32-ia32-msvc": "0.52.0", "@oxfmt/binding-win32-x64-msvc": "0.52.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug=="], + + "oxlint": ["oxlint@1.67.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="], + + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], + + "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], + + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], + + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + + "pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], + + "pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="], + + "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], + + "pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="], + + "pg-cursor": ["pg-cursor@2.20.0", "", { "peerDependencies": { "pg": "^8" } }, "sha512-HP/EbUafheaUOs7DxlG6tda/rhmsX2hCTJJJ+gCnhljGyNEs6pBHddbNuomlW3DqEhP3zYD+GqBWkYnJPIZ4tA=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="], + + "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], + + "pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pkg-pr-new": ["pkg-pr-new@0.0.62", "", { "dependencies": { "@actions/core": "^1.11.1", "@jsdevtools/ez-spawn": "^3.0.4", "@octokit/action": "^6.1.0", "ignore": "^5.3.1", "isbinaryfile": "^5.0.2", "pkg-types": "^1.1.1", "query-registry": "^3.0.1", "tinyglobby": "^0.2.9" }, "bin": { "pkg-pr-new": "bin/cli.js" } }, "sha512-K2jtf1PLCJJFDimQIpasPXDdnRTZSYPy36Ldz+QhMpLz2YN1Wi0ZQkomVt5Wi3NdBZcYuYGXekyFWfJ6fAHYjg=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="], + + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="], + + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], + + "quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + + "query-registry": ["query-registry@3.0.1", "", { "dependencies": { "query-string": "^9.0.0", "quick-lru": "^7.0.0", "url-join": "^5.0.0", "validate-npm-package-name": "^5.0.1", "zod": "^3.23.8", "zod-package-json": "^1.0.3" } }, "sha512-M9RxRITi2mHMVPU5zysNjctUT8bAPx6ltEXo/ir9+qmiM47Y7f0Ir3+OxUO5OjYAWdicBQRew7RtHtqUXydqlg=="], + + "query-string": ["query-string@9.4.0", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-ivvWyHqU9K1Log4hJFhqVIIMoEi0nzmlRhvk2pPcTuQH/Y0K5iTTMxEx7R0PRHD2Z1hMVbWnjfsEWbIKIK+3IA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-lru": ["quick-lru@7.3.0", "", {}, "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g=="], + + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], + + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], + + "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], + + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], + + "react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="], + + "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + + "regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], + + "regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], + + "rehype-expressive-code": ["rehype-expressive-code@0.41.7", "", { "dependencies": { "expressive-code": "^0.41.7" } }, "sha512-25f8ZMSF1d9CMscX7Cft0TSQIqdwjce2gDOvQ+d/w0FovsMwrSt3ODP4P3Z7wO1jsIJ4eYyaDRnIR/27bd/EMQ=="], + + "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], + + "rehype-mermaid": ["rehype-mermaid@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "mermaid-isomorphic": "^3.0.0", "mini-svg-data-uri": "^1.0.0", "space-separated-tokens": "^2.0.0", "unified": "^11.0.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "playwright": "1" }, "optionalPeers": ["playwright"] }, "sha512-fxrD5E4Fa1WXUjmjNDvLOMT4XB1WaxcfycFIWiYU0yEMQhcTDElc9aDFnbDFRLxG1Cfo1I3mfD5kg4sjlWaB+Q=="], + + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], + + "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], + + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], + + "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + + "rolldown": ["rolldown@1.0.1", "", { "dependencies": { "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-x64-msvc": "1.0.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ=="], + + "rolldown-plugin-dts": ["rolldown-plugin-dts@0.17.8", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ast-kit": "^2.2.0", "birpc": "^2.8.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.0", "magic-string": "^0.30.21", "obug": "^2.0.0" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.44", "typescript": "^5.0.0", "vue-tsc": "~3.1.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-76EEBlhF00yeY6M7VpMkWKI4r9WjuoMiOGey7j4D6zf3m0BR+ZrrY9hvSXdueJ3ljxSLq4DJBKFpX/X9+L7EKw=="], + + "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], + + "rollup-plugin-visualizer": ["rollup-plugin-visualizer@7.0.1", "", { "dependencies": { "open": "^11.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^18.0.0" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg=="], + + "rosie-skills": ["rosie-skills@0.6.4", "", { "optionalDependencies": { "rosie-skills-darwin-arm64": "0.6.4", "rosie-skills-freebsd-x64": "0.6.4", "rosie-skills-linux-x64": "0.6.4" }, "bin": { "rosie-skills": "dist/bin.js" } }, "sha512-ojfhSiQRdZ2QyWbmKAHOSAUbaLYrTc5zIH7mS1jKoP8KCFSQddwVhMyFqldckTeybTfW3zNcsZzyOTzGTN1SBA=="], + + "rosie-skills-darwin-arm64": ["rosie-skills-darwin-arm64@0.6.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rn1s5hqFKcxeiDEWWoFa1hdGPshR8TkwHLzy/cBavb9XJNAaUxbe3oQ78W9sQkRHAgRyzJYyk9tw68Qrdnizgg=="], + + "rosie-skills-freebsd-x64": ["rosie-skills-freebsd-x64@0.6.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SxCRduPBMtfjkQ+q56Yw9OLA3PyaqoALzt7kER7IDKuUVfM2O/1w8sa5xhTDiCvWkZJixnH5d5Ya6KT+/Mwcng=="], + + "rosie-skills-linux-x64": ["rosie-skills-linux-x64@0.6.4", "", { "os": "linux", "cpu": "x64" }, "sha512-D9Y9mfu7goB0s0X59uU3hcFeUTef3VbpCIDwFMzyvJrAq3XhRACWBDMHQsHlyWdHxTXPX/ILyW65RXyrJlgqng=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "satori": ["satori@0.26.0", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.17", "css-to-react-native": "^3.0.0", "emoji-regex-xs": "^2.0.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-layout": "^3.2.1" } }, "sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], + + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serialize-javascript": ["serialize-javascript@7.0.5", "", {}, "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw=="], + + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + + "serve-placeholder": ["serve-placeholder@2.0.2", "", { "dependencies": { "defu": "^6.1.4" } }, "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], + + "shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="], + + "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "smartypants": ["smartypants@0.2.2", "", { "bin": { "smartypants": "bin/smartypants.js", "smartypantsu": "bin/smartypantsu.js" } }, "sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q=="], + + "smob": ["smob@1.6.2", "", {}, "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw=="], + + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "solid-devtools": ["solid-devtools@0.34.5", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.6", "@solid-devtools/debugger": "^0.28.1", "@solid-devtools/shared": "^0.20.0" }, "peerDependencies": { "solid-js": "^1.9.0", "vite": "^2.2.3 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["vite"] }, "sha512-KNVdS9MQzzeVS++Vmg4JeU0fM6ZMuBEmkBA7SmqPS2s5UHpRjv1PNH8gShmlN9L/tki6OUAzJP3H1aKq2AcOSg=="], + + "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], + + "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + + "solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="], + + "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], + + "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], + + "srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + + "starlight-blog": ["starlight-blog@0.24.0", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.0.8", "@astrojs/rss": "^4.0.11", "astro-remote": "^0.3.3", "github-slugger": "^2.0.0", "marked": "^15.0.4", "marked-plaintify": "^1.1.1", "mdast-util-mdx-expression": "^2.0.1", "ultrahtml": "^1.6.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@astrojs/starlight": ">=0.33.0" } }, "sha512-judPLTD+hyeKyiX//0x6adcOs4QR3EqmNxzFJNP+2sauUDA/ux1CS+wNzwMkHAadxdCir13rj9YsA3l8X0h9fQ=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], + + "streamx": ["streamx@2.26.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-VvNG1K72Po/xwJzxZFnZ++Tbrv4lwSptsbkFuzXCJAYZvCK5nnxsvXU6ajqkv7chyiI1Y0YXq2Jh8Iy8Y7NF/A=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="], + + "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], + + "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], + + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], + + "terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="], + + "terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="], + + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyclip": ["tinyclip@0.1.13", "", {}, "sha512-8OqlXQ35euK9+e7L68u8UwcODxkHoIkjbGsgXuARKNyQ5G6xt8nw1YPeMbxMLgCPFkToU+UEK5j05t2t8edKpQ=="], + + "tinyexec": ["tinyexec@1.2.3", "", {}, "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "treeify": ["treeify@1.1.0", "", {}, "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + + "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], + + "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tsdown": ["tsdown@0.15.12", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.3", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "rolldown": "1.0.0-beta.45", "rolldown-plugin-dts": "^0.17.2", "semver": "^7.7.3", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig": "^7.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0", "unrun": "^0.2.1" }, "optionalPeers": ["@arethetypeswrong/core", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused", "unrun"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-c8VLlQm8/lFrOAg5VMVeN4NAbejZyVQkzd+ErjuaQgJFI/9MhR9ivr0H/CM7UlOF1+ELlF6YaI7sU/4itgGQ8w=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.22.3", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg=="], + + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + + "twoslash-eslint": ["twoslash-eslint@0.3.8", "", { "dependencies": { "twoslash-protocol": "0.3.8" }, "peerDependencies": { "eslint": ">=8.50.0" } }, "sha512-4rW6i4ALza33+95G3IOG1l2FgBb84+SYzmX/GCe2suMTvZ2P4kJUTGIlr7tIqgPU90FRzDs3iL2i6X/Chuosug=="], + + "twoslash-protocol": ["twoslash-protocol@0.3.8", "", {}, "sha512-HmvAHoiEviK8LqvAQyc9/irkdvwTUiR1fHmNwH/0gq8EHxyBt4PWVPixjEXg6wJu1u6yBrILEWXGK9Kw58/8yQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + + "type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], + + "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], + + "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], + + "unconfig": ["unconfig@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "defu": "^6.1.4", "jiti": "^2.6.1", "quansync": "^1.0.0", "unconfig-core": "7.5.0" } }, "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA=="], + + "unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], + + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], + + "unctx": ["unctx@2.5.0", "", { "dependencies": { "acorn": "^8.15.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21", "unplugin": "^2.3.11" } }, "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg=="], + + "undici": ["undici@7.26.0", "", {}, "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], + + "unimport": ["unimport@6.3.0", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.2", "pathe": "^2.0.3", "picomatch": "^4.0.4", "pkg-types": "^2.3.1", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.16", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "oxc-parser": "*", "rolldown": "^1.0.0" }, "optionalPeers": ["oxc-parser", "rolldown"] }, "sha512-M+Dxk5W9WRd+8j56W9tp8lGW/dmMc7g5zj7BWQnEjKQhryBstqsi1V0izb0zHwSkEN8cSYV7K75/bykairV2tA=="], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], + + "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], + + "unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="], + + "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], + + "untun": ["untun@0.1.3", "", { "dependencies": { "citty": "^0.1.5", "consola": "^3.2.3", "pathe": "^1.1.1" }, "bin": { "untun": "bin/untun.mjs" } }, "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ=="], + + "untyped": ["untyped@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "defu": "^6.1.4", "jiti": "^2.4.2", "knitwork": "^1.2.0", "scule": "^1.3.0" }, "bin": { "untyped": "dist/cli.mjs" } }, "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g=="], + + "unwasm": ["unwasm@0.5.3", "", { "dependencies": { "exsolve": "^1.0.8", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "pkg-types": "^2.3.0" } }, "sha512-keBgTSfp3r6+s9ZcSma+0chwxQdmLbB5+dAD9vjtB21UTMYuKAxHXCU1K2CbCtnP09EaWeRvACnXk0EJtUx+hw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], + + "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="], + + "vite-dev-rpc": ["vite-dev-rpc@2.0.0", "", { "dependencies": { "birpc": "^4.0.0", "vite-hot-client": "^2.2.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0" } }, "sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA=="], + + "vite-hot-client": ["vite-hot-client@2.2.0", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0" } }, "sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w=="], + + "vite-plugin-inspect": ["vite-plugin-inspect@11.4.1", "", { "dependencies": { "ansis": "^4.3.0", "error-stack-parser-es": "^1.0.5", "obug": "^2.1.1", "ohash": "^2.0.11", "open": "^11.0.0", "perfect-debounce": "^2.1.0", "sirv": "^3.0.2", "unplugin-utils": "^0.3.1", "vite-dev-rpc": "^2.0.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA=="], + + "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], + + "vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@8.1.2", "", { "dependencies": { "@vue/devtools-core": "^8.1.2", "@vue/devtools-kit": "^8.1.2", "@vue/devtools-shared": "^8.1.2", "sirv": "^3.0.2", "vite-plugin-inspect": "^11.3.3", "vite-plugin-vue-inspector": "^6.0.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-gt5h1CNryR9Hy0tvhSbqY3j0F7aj0pGxBxWLa1lXSiZVkhdWDf0vbCOZyjh8ivFGE6FDHTGy3zkcZGlMZdVHig=="], + + "vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@6.0.0", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw=="], + + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "vitest": ["vitest@4.1.7", "", { "dependencies": { "@vitest/expect": "4.1.7", "@vitest/mocker": "4.1.7", "@vitest/pretty-format": "4.1.7", "@vitest/runner": "4.1.7", "@vitest/snapshot": "4.1.7", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.7", "@vitest/browser-preview": "4.1.7", "@vitest/browser-webdriverio": "4.1.7", "@vitest/coverage-istanbul": "4.1.7", "@vitest/coverage-v8": "4.1.7", "@vitest/ui": "4.1.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA=="], + + "volar-service-css": ["volar-service-css@0.0.70", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw=="], + + "volar-service-emmet": ["volar-service-emmet@0.0.70", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg=="], + + "volar-service-html": ["volar-service-html@0.0.70", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ=="], + + "volar-service-prettier": ["volar-service-prettier@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg=="], + + "volar-service-typescript": ["volar-service-typescript@0.0.70", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg=="], + + "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ=="], + + "volar-service-yaml": ["volar-service-yaml@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.20.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ=="], + + "vscode-css-languageservice": ["vscode-css-languageservice@6.3.10", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA=="], + + "vscode-html-languageservice": ["vscode-html-languageservice@5.6.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg=="], + + "vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="], + + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + + "vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "vue": ["vue@3.6.0-beta.13", "", { "dependencies": { "@vue/compiler-dom": "3.6.0-beta.13", "@vue/compiler-sfc": "3.6.0-beta.13", "@vue/runtime-dom": "3.6.0-beta.13", "@vue/runtime-vapor": "3.6.0-beta.13", "@vue/server-renderer": "3.6.0-beta.13", "@vue/shared": "3.6.0-beta.13" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-FVbMX/iDFIy29e0ImuIteca1IybMgecfHjKKD22p4eVPh1vaSlF/IbtwjKHDzGkhdoN1xPf5Swad4jNBnx6d3w=="], + + "vue-router": ["vue-router@5.1.0", "", { "dependencies": { "@babel/generator": "^8.0.0-rc.4", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.1.2", "ast-walker-scope": "^0.9.0", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.2", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.16", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.9.0" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.34", "pinia": "^3.0.4", "vite": "^7.0.0 || ^8.0.0", "vue": "^3.5.34" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia", "vite"] }, "sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g=="], + + "vue-tsc": ["vue-tsc@3.3.2", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.3.2" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-n7nQoA3YWW/eiDR8jMiv/uJvlg0uLGs+YgUrsTrf9EZaYSt3tuvMZb5V8+7Mvh/EH5pnY/hoVdgfjH+XcK+wwA=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "website": ["website@workspace:website"], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + + "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "workerd": ["workerd@1.20260526.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260526.1", "@cloudflare/workerd-darwin-arm64": "1.20260526.1", "@cloudflare/workerd-linux-64": "1.20260526.1", "@cloudflare/workerd-linux-arm64": "1.20260526.1", "@cloudflare/workerd-windows-64": "1.20260526.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-IHzymht98p10JH1zzwdCpbViAqw97HrwKl7+KfZeASFMsYSrIsAULWdPn0LRC5FTUzBpamLNyKCCKxbgXHgRHQ=="], + + "wrangler": ["wrangler@4.95.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260526.0", "path-to-regexp": "6.3.0", "rosie-skills": "^0.6.3", "unenv": "2.0.0-rc.24", "workerd": "1.20260526.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260526.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-vgXzFVSCdUbeCadgVXvu8fK5tzNm8T9W+7lriyGWZMx0B1+CAdr4d8JTlZszHfgjypRAHmAxb49etZGIRD9pgg=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "xdg-app-paths": ["xdg-app-paths@8.3.0", "", { "dependencies": { "xdg-portable": "^10.6.0" }, "optionalDependencies": { "fsevents": "*" } }, "sha512-mgxlWVZw0TNWHoGmXq+NC3uhCIc55dDpAlDkMQUaIAcQzysb0kxctwv//fvuW61/nAAeUBJMQ8mnZjMmuYwOcQ=="], + + "xdg-portable": ["xdg-portable@10.6.0", "", { "dependencies": { "os-paths": "^7.4.0" }, "optionalDependencies": { "fsevents": "*" } }, "sha512-xrcqhWDvtZ7WLmt8G4f3hHy37iK7D2idtosRgkeiSPZEPmBShp0VfmRBLWAPC6zLF48APJ21yfea+RfQMF4/Aw=="], + + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], + + "xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "yaml-language-server": ["yaml-language-server@1.20.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + + "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-package-json": ["zod-package-json@1.2.0", "", { "dependencies": { "zod": "^3.25.64" } }, "sha512-tamtgPM3MkP+obfO2dLr/G+nYoYkpJKmuHdYEy6IXRKfLybruoJ5NUj0lM0LxwOpC9PpoGLbll1ecoeyj43Wsg=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "@astrojs/internal-helpers/shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="], + + "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.1", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ=="], + + "@astrojs/markdown-remark/shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="], + + "@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + + "@astrojs/react/vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "@astrojs/rss/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@astrojs/sitemap/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@astrojs/starlight/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.11", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="], + + "@astrojs/starlight/@astrojs/mdx": ["@astrojs/mdx@4.3.14", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.11", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-FBrqJQORVm+rkRa2TS5CjU9PBA6hkhrwLVBSS9A77gN2+iehvjq1w6yya/d0YKC7osiVorKkr3Qd9wNbl0ZkGA=="], + + "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], + + "@babel/core/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/generator/@babel/parser": ["@babel/parser@8.0.0-rc.6", "", { "dependencies": { "@babel/types": "^8.0.0-rc.6" }, "bin": "./bin/babel-parser.js" }, "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog=="], + + "@babel/generator/@babel/types": ["@babel/types@8.0.0-rc.6", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.6", "@babel/helper-validator-identifier": "^8.0.0-rc.6" } }, "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/traverse/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@cloudflare/vite-plugin/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@distilled.cloud/cloudflare-runtime/capnweb": ["capnweb@0.7.0", "", {}, "sha512-zO7tt5ch2tImacaR/oMd7e1dqi/fWU7hjZdvQMv6Yo3v9uUGA8cPIUQGvfQTu2c+NgyE/j/oDmMaUlf1PXyfJw=="], + + "@effect/platform-node/undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="], + + "@effect/sql-pg/pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], + + "@effect/sql-pg/pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@expressive-code/plugin-shiki/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@mapbox/node-pre-gyp/tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], + + "@octokit/action/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="], + + "@octokit/action/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="], + + "@octokit/action/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="], + + "@octokit/action/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], + + "@octokit/auth-action/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], + + "@octokit/auth-action/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + + "@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/endpoint/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/graphql/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], + + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@rollup/plugin-inject/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], + + "@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], + + "@solidjs/start/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "@solidjs/start/vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "@solidjs/vite-plugin-nitro-2/vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@tanstack/directive-functions-plugin/@tanstack/router-utils": ["@tanstack/router-utils@1.133.19", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA=="], + + "@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + + "@tanstack/router-generator/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@tanstack/router-plugin/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "@tanstack/router-plugin/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@tanstack/start-plugin-core/srvx": ["srvx@0.11.16", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw=="], + + "@tanstack/start-plugin-core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@typescript/analyze-trace/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@typescript/analyze-trace/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "@typescript/analyze-trace/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + + "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue-macros/common/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/compiler-core": "3.5.35", "@vue/compiler-dom": "3.5.35", "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw=="], + + "@vue/babel-plugin-jsx/@vue/shared": ["@vue/shared@3.5.35", "", {}, "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/compiler-core": "3.5.35", "@vue/compiler-dom": "3.5.35", "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw=="], + + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/compiler-vapor/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/language-core/@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="], + + "@vue/language-core/@vue/shared": ["@vue/shared@3.5.35", "", {}, "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="], + + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "archiver/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "archiver-utils/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.6", "", {}, "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q=="], + + "astro/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.11", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="], + + "astro/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "astro/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "astro/p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], + + "astro/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "astro/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + + "astro-remote/marked": ["marked@12.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "better-auth/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "boxen/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "boxen/widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + + "buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "c12/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "changelogen/c12": ["c12@1.11.2", "", { "dependencies": { "chokidar": "^3.6.0", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^1.21.6", "mlly": "^1.7.1", "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.4" }, "optionalPeers": ["magicast"] }, "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew=="], + + "changelogen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "changelogen/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "cloudflare-solid-ssr/vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "cloudflare-tanstack-example/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "cloudflare-vue/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + + "cloudflare-vue/oxfmt": ["oxfmt@0.42.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.42.0", "@oxfmt/binding-android-arm64": "0.42.0", "@oxfmt/binding-darwin-arm64": "0.42.0", "@oxfmt/binding-darwin-x64": "0.42.0", "@oxfmt/binding-freebsd-x64": "0.42.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.42.0", "@oxfmt/binding-linux-arm-musleabihf": "0.42.0", "@oxfmt/binding-linux-arm64-gnu": "0.42.0", "@oxfmt/binding-linux-arm64-musl": "0.42.0", "@oxfmt/binding-linux-ppc64-gnu": "0.42.0", "@oxfmt/binding-linux-riscv64-gnu": "0.42.0", "@oxfmt/binding-linux-riscv64-musl": "0.42.0", "@oxfmt/binding-linux-s390x-gnu": "0.42.0", "@oxfmt/binding-linux-x64-gnu": "0.42.0", "@oxfmt/binding-linux-x64-musl": "0.42.0", "@oxfmt/binding-openharmony-arm64": "0.42.0", "@oxfmt/binding-win32-arm64-msvc": "0.42.0", "@oxfmt/binding-win32-ia32-msvc": "0.42.0", "@oxfmt/binding-win32-x64-msvc": "0.42.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg=="], + + "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "compress-commons/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "crc32-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "eslint/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "example-basic/vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "globby/unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], + + "h3-v2/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="], + + "h3-v2/srvx": ["srvx@0.11.16", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw=="], + + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + + "listhen/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "local-pkg/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "local-pkg/quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "miniflare/undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], + + "miniflare/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "nitropack/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], + + "nitropack/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "nitropack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "nitropack/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "nitropack/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "nitropack/youch": ["youch@4.1.1", "", { "dependencies": { "@poppinss/colors": "^4.1.6", "@poppinss/dumper": "^0.7.0", "@speed-highlight/core": "^1.2.14", "cookie-es": "^3.0.1", "youch-core": "^0.3.3" } }, "sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "nypm/citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "oniguruma-to-es/emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], + + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "pgpass/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "react-devtools-core/ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="], + + "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "rolldown/@oxc-project/types": ["@oxc-project/types@0.130.0", "", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="], + + "rolldown-plugin-dts/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "rollup-plugin-visualizer/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + + "rollup-plugin-visualizer/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "sitemap/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + + "solid-refresh/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "split2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "starlight-blog/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.11", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="], + + "starlight-blog/@astrojs/mdx": ["@astrojs/mdx@4.3.14", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.11", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-FBrqJQORVm+rkRa2TS5CjU9PBA6hkhrwLVBSS9A77gN2+iehvjq1w6yya/d0YKC7osiVorKkr3Qd9wNbl0ZkGA=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "through2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "tsdown/rolldown": ["rolldown@1.0.0-beta.45", "", { "dependencies": { "@oxc-project/types": "=0.95.0", "@rolldown/pluginutils": "1.0.0-beta.45" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.45", "@rolldown/binding-darwin-arm64": "1.0.0-beta.45", "@rolldown/binding-darwin-x64": "1.0.0-beta.45", "@rolldown/binding-freebsd-x64": "1.0.0-beta.45", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.45", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.45", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.45", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.45", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.45", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.45", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.45", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.45", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.45", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.45" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ=="], + + "unctx/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + + "unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "unimport/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "untun/citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "untun/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "untyped/citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "unwasm/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "vite/rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="], + + "vite-dev-rpc/birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], + + "vite-plugin-inspect/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + + "vite-plugin-vue-inspector/@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="], + + "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "vue-router/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "wrangler/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yaml-language-server/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], + + "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "@astrojs/internal-helpers/shiki/@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], + + "@astrojs/internal-helpers/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="], + + "@astrojs/internal-helpers/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="], + + "@astrojs/internal-helpers/shiki/@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="], + + "@astrojs/internal-helpers/shiki/@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="], + + "@astrojs/internal-helpers/shiki/@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], + + "@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="], + + "@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="], + + "@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="], + + "@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], + + "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + + "@astrojs/react/vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "@astrojs/starlight/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.6", "", {}, "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q=="], + + "@astrojs/starlight/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "@astrojs/starlight/@astrojs/mdx/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.6", "", {}, "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g=="], + + "@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.6", "", {}, "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg=="], + + "@effect/sql-pg/pg-types/postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], + + "@effect/sql-pg/pg-types/postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="], + + "@effect/sql-pg/pg-types/postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="], + + "@effect/sql-pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@mapbox/node-pre-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "@mapbox/node-pre-gyp/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "@mapbox/node-pre-gyp/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "@octokit/action/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], + + "@octokit/action/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], + + "@octokit/action/@octokit/core/@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], + + "@octokit/action/@octokit/core/@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], + + "@octokit/action/@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + + "@octokit/action/@octokit/core/before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], + + "@octokit/action/@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + + "@octokit/auth-action/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@solidjs/start/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@solidjs/start/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@solidjs/start/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@solidjs/start/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@solidjs/start/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@solidjs/start/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@solidjs/start/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@solidjs/start/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@solidjs/start/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@solidjs/start/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@solidjs/start/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@solidjs/start/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@solidjs/start/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@solidjs/start/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@solidjs/start/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@solidjs/start/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@solidjs/start/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@solidjs/start/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@solidjs/start/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@solidjs/start/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@solidjs/start/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@solidjs/start/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@solidjs/start/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@solidjs/start/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@solidjs/start/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@solidjs/start/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@solidjs/start/vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "@tanstack/directive-functions-plugin/@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "@typescript/analyze-trace/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@typescript/analyze-trace/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@typescript/analyze-trace/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "@typescript/analyze-trace/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@typescript/analyze-trace/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.35", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.35", "", { "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.35", "", {}, "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="], + + "@vue-macros/common/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.35", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.35", "", { "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.35", "", {}, "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.35", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw=="], + + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "ansi-align/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "archiver-utils/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "archiver/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "archiver/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "astro/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], + + "astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "astro/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "astro/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "astro/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "astro/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "astro/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "astro/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "astro/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "astro/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "astro/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "astro/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "astro/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "astro/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "astro/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "astro/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "astro/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "astro/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "astro/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "astro/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "astro/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "astro/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "astro/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "changelogen/c12/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "changelogen/c12/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "changelogen/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "changelogen/c12/giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], + + "changelogen/c12/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "changelogen/c12/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + + "changelogen/c12/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "changelogen/c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cloudflare-solid-ssr/vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "cloudflare-vue/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.42.0", "", { "os": "android", "cpu": "arm" }, "sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.42.0", "", { "os": "android", "cpu": "arm64" }, "sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.42.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.42.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.42.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.42.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.42.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.42.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.42.0", "", { "os": "linux", "cpu": "none" }, "sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.42.0", "", { "os": "linux", "cpu": "none" }, "sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.42.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.42.0", "", { "os": "none", "cpu": "arm64" }, "sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.42.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.42.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw=="], + + "cloudflare-vue/oxfmt/@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.42.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ=="], + + "compress-commons/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "compress-commons/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "crc32-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "crc32-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "example-basic/vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "listhen/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "nitropack/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "nitropack/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "nitropack/youch/@poppinss/dumper": ["@poppinss/dumper@0.7.0", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-0UTYalzk2t6S4rA2uHOz5bSSW2CHdv4vggJI6Alg90yvl0UgXs6XSXpH96OH+bRkX4J/06djv29pqXJ0lq5Kag=="], + + "nitropack/youch/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + + "rollup-plugin-visualizer/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + + "rollup-plugin-visualizer/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "rollup-plugin-visualizer/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "rollup-plugin-visualizer/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "sitemap/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "starlight-blog/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.6", "", {}, "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q=="], + + "starlight-blog/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], + + "starlight-blog/@astrojs/markdown-remark/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "starlight-blog/@astrojs/mdx/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "tsdown/rolldown/@oxc-project/types": ["@oxc-project/types@0.95.0", "", {}, "sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ=="], + + "tsdown/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.45", "", { "os": "android", "cpu": "arm64" }, "sha512-bfgKYhFiXJALeA/riil908+2vlyWGdwa7Ju5S+JgWZYdR4jtiPOGdM6WLfso1dojCh+4ZWeiTwPeV9IKQEX+4g=="], + + "tsdown/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.45", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xjCv4CRVsSnnIxTuyH1RDJl5OEQ1c9JYOwfDAHddjJDxCw46ZX9q80+xq7Eok7KC4bRSZudMJllkvOKv0T9SeA=="], + + "tsdown/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.45", "", { "os": "darwin", "cpu": "x64" }, "sha512-ddcO9TD3D/CLUa/l8GO8LHzBOaZqWg5ClMy3jICoxwCuoz47h9dtqPsIeTiB6yR501LQTeDsjA4lIFd7u3Ljfw=="], + + "tsdown/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.45", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MBTWdrzW9w+UMYDUvnEuh0pQvLENkl2Sis15fHTfHVW7ClbGuez+RWopZudIDEGkpZXdeI4CkRXk+vdIIebrmg=="], + + "tsdown/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.45", "", { "os": "linux", "cpu": "arm" }, "sha512-4YgoCFiki1HR6oSg+GxxfzfnVCesQxLF1LEnw9uXS/MpBmuog0EOO2rYfy69rWP4tFZL9IWp6KEfGZLrZ7aUog=="], + + "tsdown/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.45", "", { "os": "linux", "cpu": "arm64" }, "sha512-LE1gjAwQRrbCOorJJ7LFr10s5vqYf5a00V5Ea9wXcT2+56n5YosJkcp8eQ12FxRBv2YX8dsdQJb+ZTtYJwb6XQ=="], + + "tsdown/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.45", "", { "os": "linux", "cpu": "arm64" }, "sha512-tdy8ThO/fPp40B81v0YK3QC+KODOmzJzSUOO37DinQxzlTJ026gqUSOM8tzlVixRbQJltgVDCTYF8HNPRErQTA=="], + + "tsdown/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.45", "", { "os": "linux", "cpu": "x64" }, "sha512-lS082ROBWdmOyVY/0YB3JmsiClaWoxvC+dA8/rbhyB9VLkvVEaihLEOr4CYmrMse151C4+S6hCw6oa1iewox7g=="], + + "tsdown/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.45", "", { "os": "linux", "cpu": "x64" }, "sha512-Hi73aYY0cBkr1/SvNQqH8Cd+rSV6S9RB5izCv0ySBcRnd/Wfn5plguUoGYwBnhHgFbh6cPw9m2dUVBR6BG1gxA=="], + + "tsdown/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.45", "", { "os": "none", "cpu": "arm64" }, "sha512-fljEqbO7RHHogNDxYtTzr+GNjlfOx21RUyGmF+NrkebZ8emYYiIqzPxsaMZuRx0rgZmVmliOzEp86/CQFDKhJQ=="], + + "tsdown/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.45", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-ZJDB7lkuZE9XUnWQSYrBObZxczut+8FZ5pdanm8nNS1DAo8zsrPuvGwn+U3fwU98WaiFsNrA4XHngesCGr8tEQ=="], + + "tsdown/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.45", "", { "os": "win32", "cpu": "arm64" }, "sha512-zyzAjItHPUmxg6Z8SyRhLdXlJn3/D9KL5b9mObUrBHhWS/GwRH4665xCiFqeuktAhhWutqfc+rOV2LjK4VYQGQ=="], + + "tsdown/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.45", "", { "os": "win32", "cpu": "x64" }, "sha512-wiU40G1nQo9rtfvF9jLbl79lUgjfaD/LTyUEw2Wg/gdF5OhjzpKMVugZQngO+RNdwYaNj+Fs+kWBWfp4VXPMHA=="], + + "tsdown/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.45", "", {}, "sha512-Le9ulGCrD8ggInzWw/k2J8QcbPz7eGIOWqfJ2L+1R0Opm7n6J37s2hiDWlh6LJN0Lk9L5sUzMvRHKW7UxBZsQA=="], + + "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "vite-plugin-inspect/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + + "vite-plugin-vue-inspector/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.35", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw=="], + + "vite-plugin-vue-inspector/@vue/compiler-dom/@vue/shared": ["@vue/shared@3.5.35", "", {}, "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="], + + "vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="], + + "vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="], + + "vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="], + + "vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="], + + "vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="], + + "vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="], + + "vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="], + + "vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="], + + "vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="], + + "vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="], + + "vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="], + + "vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="], + + "vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="], + + "vite/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="], + + "vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="], + + "vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="], + + "vue-router/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yaml-language-server/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "zip-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "zip-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "@astrojs/internal-helpers/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "@astrojs/react/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@astrojs/react/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@astrojs/react/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@astrojs/react/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@astrojs/react/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@astrojs/react/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@astrojs/react/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@astrojs/react/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@astrojs/react/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@astrojs/react/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@astrojs/react/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@astrojs/react/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@astrojs/react/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@astrojs/react/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@astrojs/react/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "@octokit/action/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], + + "@octokit/action/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@solidjs/start/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@solidjs/start/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@solidjs/start/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@solidjs/start/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@solidjs/start/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@solidjs/start/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@solidjs/start/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@solidjs/start/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@solidjs/start/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@solidjs/start/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@solidjs/start/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@solidjs/start/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@solidjs/start/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@solidjs/start/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@solidjs/start/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@solidjs/start/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@solidjs/start/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@solidjs/start/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@solidjs/vite-plugin-nitro-2/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@typescript/analyze-trace/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@typescript/analyze-trace/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@typescript/analyze-trace/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@typescript/analyze-trace/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@typescript/analyze-trace/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "@vue/language-core/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + + "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "archiver-utils/readable-stream/buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "archiver-utils/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "archiver/readable-stream/buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "archiver/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "astro/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "astro/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "astro/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "astro/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "astro/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "astro/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "astro/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "astro/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "astro/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "astro/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "astro/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "astro/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "astro/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "astro/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "astro/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "astro/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "astro/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "astro/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "astro/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "astro/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "astro/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "astro/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "astro/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "astro/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "astro/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "astro/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "astro/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "changelogen/c12/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "changelogen/c12/giget/citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "changelogen/c12/giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "cloudflare-solid-ssr/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "compress-commons/readable-stream/buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "compress-commons/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "crc32-stream/readable-stream/buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "crc32-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "example-basic/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "example-basic/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "example-basic/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "example-basic/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "example-basic/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "example-basic/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "example-basic/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "example-basic/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "example-basic/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "example-basic/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "example-basic/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "example-basic/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "example-basic/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "example-basic/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "example-basic/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "example-basic/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "example-basic/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "example-basic/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "example-basic/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "example-basic/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "example-basic/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "example-basic/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "example-basic/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "example-basic/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "example-basic/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "example-basic/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "nitropack/youch/@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "vite-plugin-vue-inspector/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "vite-plugin-vue-inspector/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "zip-stream/readable-stream/buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "zip-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "@astrojs/internal-helpers/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "@astrojs/internal-helpers/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "@typescript/analyze-trace/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@typescript/analyze-trace/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@typescript/analyze-trace/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "astro/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "astro/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "changelogen/c12/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "starlight-blog/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + } +} diff --git a/.repos/alchemy-effect/examples/aws-ec2/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-ec2/alchemy.run.ts new file mode 100644 index 00000000000..af4cf02dc0e --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ec2/alchemy.run.ts @@ -0,0 +1,23 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Output from "alchemy/Output"; +import * as Effect from "effect/Effect"; +import { NetworkLive } from "./src/Network.ts"; +import Server from "./src/Server.ts"; + +const aws = AWS.providers(); + +export default Alchemy.Stack( + "AwsEc2Example", + { providers: aws, state: Alchemy.localState() }, + Effect.gen(function* () { + const instance = yield* Server; + + return { + instanceId: instance.instanceId, + publicIpAddress: instance.publicIpAddress, + instanceUrl: Output.interpolate`http://${instance.publicIpAddress}:3000`, + enqueueExample: Output.interpolate`http://${instance.publicIpAddress}:3000/enqueue?message=hello`, + }; + }).pipe(Effect.provide(NetworkLive)), +); diff --git a/.repos/alchemy-effect/examples/aws-ec2/package.json b/.repos/alchemy-effect/examples/aws-ec2/package.json new file mode 100644 index 00000000000..5f040f52f29 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ec2/package.json @@ -0,0 +1,23 @@ +{ + "name": "aws-ec2-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-ec2" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-ec2/src/Network.ts b/.repos/alchemy-effect/examples/aws-ec2/src/Network.ts new file mode 100644 index 00000000000..d8a1516e0d8 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ec2/src/Network.ts @@ -0,0 +1,45 @@ +import * as AWS from "alchemy/AWS"; +import type { Output } from "alchemy/Output"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +export interface ExampleNetwork { + network: AWS.EC2.Network; + publicSubnetIds: Output[]; + appSecurityGroupId: Output; +} + +export class Network extends Context.Service()( + "Network", +) {} + +export const NetworkLive = Layer.effect( + Network, + Effect.gen(function* () { + const network = yield* AWS.EC2.Network("Network", { + cidrBlock: "10.42.0.0/16", + availabilityZones: 2, + nat: "single", + }); + + const appSecurityGroup = yield* AWS.EC2.SecurityGroup("AppSecurityGroup", { + vpcId: network.vpcId, + description: "Security group for the EC2 application instance", + ingress: [ + { + ipProtocol: "tcp", + fromPort: 3000, + toPort: 3000, + cidrIpv4: "0.0.0.0/0", + }, + ], + }); + + return { + network, + publicSubnetIds: network.publicSubnetIds, + appSecurityGroupId: appSecurityGroup.groupId, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-ec2/src/Server.ts b/.repos/alchemy-effect/examples/aws-ec2/src/Server.ts new file mode 100644 index 00000000000..6fc80675933 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ec2/src/Server.ts @@ -0,0 +1,89 @@ +import * as AWS from "alchemy/AWS"; +import { SQSQueueEventSource } from "alchemy/Server/SQSQueueEventSource"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Network, NetworkLive } from "./Network.ts"; + +export default class Server extends AWS.EC2.Instance()( + "ServerInstance", + Effect.gen(function* () { + const imageId = yield* AWS.EC2.amazonLinux(); + const network = yield* Network; + + return { + main: import.meta.filename, + imageId, + instanceType: "t3.small", + subnetId: network.publicSubnetIds[0], + securityGroupIds: [network.appSecurityGroupId], + associatePublicIpAddress: true, + port: 3000, + }; + }), + Effect.gen(function* () { + const queue = yield* AWS.SQS.Queue("JobsQueue", { + receiveMessageWaitTimeSeconds: 20, + visibilityTimeout: 60, + }); + + yield* AWS.SQS.messages(queue).subscribe((stream) => + stream.pipe(Stream.mapEffect(Effect.logInfo), Stream.runDrain), + ); + + const sendMessage = yield* AWS.SQS.SendMessage.bind(queue); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + const url = new URL(request.originalUrl); + + if (request.method === "GET" && url.pathname === "/") { + return yield* HttpServerResponse.json({ + ok: true, + routes: ["GET /", "GET /enqueue?message=hello"], + }); + } + + if (request.method === "GET" && url.pathname === "/enqueue") { + const message = url.searchParams.get("message") ?? "hello from EC2"; + const body = JSON.stringify({ + message, + enqueuedAt: new Date().toISOString(), + }); + + const result = yield* sendMessage({ + MessageBody: body, + }); + + return yield* HttpServerResponse.json({ + ok: true, + message, + messageId: result.MessageId, + }); + } + + return HttpServerResponse.text("Not found", { status: 404 }); + }).pipe( + Effect.catch(() => + Effect.succeed( + HttpServerResponse.text("Internal server error", { status: 500 }), + ), + ), + ), + }; + }).pipe( + Effect.provide( + Layer.provideMerge( + Layer.mergeAll(NetworkLive, SQSQueueEventSource), + Layer.mergeAll( + AWS.SQS.DeleteMessageBatchLive, + AWS.SQS.ReceiveMessageLive, + AWS.SQS.SendMessageLive, + ), + ), + ), + ), +) {} diff --git a/.repos/alchemy-effect/examples/aws-ec2/tsconfig.json b/.repos/alchemy-effect/examples/aws-ec2/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ec2/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-ecs/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-ecs/alchemy.run.ts new file mode 100644 index 00000000000..78e98e0b503 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ecs/alchemy.run.ts @@ -0,0 +1,80 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Output from "alchemy/Output"; +import * as Effect from "effect/Effect"; +import QueueConsumerTask from "./src/QueueConsumerTask.ts"; +import ApiTask from "./src/Task.ts"; + +const aws = AWS.providers(); + +export default Alchemy.Stack( + "AwsEcsExample", + { + providers: aws, + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const network = yield* AWS.EC2.Network("ExampleNetwork", { + cidrBlock: "10.42.0.0/16", + availabilityZones: 2, + }); + + const serviceSecurityGroup = yield* AWS.EC2.SecurityGroup( + "ExampleServiceSecurityGroup", + { + vpcId: network.vpcId, + description: "Security group for the ECS example services", + ingress: [ + { + ipProtocol: "tcp", + fromPort: 80, + toPort: 80, + cidrIpv4: "0.0.0.0/0", + }, + { + ipProtocol: "tcp", + fromPort: 3000, + toPort: 3000, + cidrIpv4: "0.0.0.0/0", + }, + ], + }, + ); + + const queue = yield* AWS.SQS.Queue("ExampleJobsQueue", { + receiveMessageWaitTimeSeconds: 20, + visibilityTimeout: 60, + }); + + const cluster = yield* AWS.ECS.Cluster("ExampleCluster", {}); + const apiTask = yield* ApiTask; + const queuePollerTask = yield* QueueConsumerTask; + + const apiService = yield* AWS.ECS.Service("ExampleApiService", { + cluster, + task: apiTask, + vpcId: network.vpcId, + subnets: network.publicSubnetIds, + securityGroups: [serviceSecurityGroup.groupId], + assignPublicIp: true, + public: true, + healthCheckPath: "/", + }); + + yield* AWS.ECS.Service("ExampleQueuePollerService", { + cluster, + task: queuePollerTask, + vpcId: network.vpcId, + subnets: network.publicSubnetIds, + securityGroups: [serviceSecurityGroup.groupId], + assignPublicIp: true, + desiredCount: 1, + }); + + return { + url: apiService.url, + queueUrl: queue.queueUrl, + enqueueExample: Output.interpolate`${apiService.url}/enqueue?message=hello`, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-ecs/package.json b/.repos/alchemy-effect/examples/aws-ecs/package.json new file mode 100644 index 00000000000..a5b313d4a76 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ecs/package.json @@ -0,0 +1,23 @@ +{ + "name": "aws-ecs-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-ecs" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-ecs/src/JobsQueue.ts b/.repos/alchemy-effect/examples/aws-ecs/src/JobsQueue.ts new file mode 100644 index 00000000000..da588816dbb --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ecs/src/JobsQueue.ts @@ -0,0 +1,16 @@ +import * as AWS from "alchemy/AWS"; +import * as Context from "effect/Context"; +import * as Layer from "effect/Layer"; + +/** + * Deploy-time binding for the ECS example’s jobs queue. Provide with + * `Layer.succeed(ExampleJobsQueue, queue)` from the stack after the queue exists. + */ +export class JobsQueue extends Context.Service()( + "JobsQueue", +) {} + +export const JobsQueueLive = Layer.effect( + JobsQueue, + AWS.SQS.Queue("JobsQueue"), +); diff --git a/.repos/alchemy-effect/examples/aws-ecs/src/QueueConsumerTask.ts b/.repos/alchemy-effect/examples/aws-ecs/src/QueueConsumerTask.ts new file mode 100644 index 00000000000..3154ff749fc --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ecs/src/QueueConsumerTask.ts @@ -0,0 +1,41 @@ +import * as AWS from "alchemy/AWS"; +import * as Server from "alchemy/Server"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import { JobsQueue, JobsQueueLive } from "./JobsQueue.ts"; + +export default class QueueConsumerTask extends AWS.ECS.Task()( + "QueueConsumerTask", + { + main: import.meta.filename, + cpu: 256, + memory: 512, + taskRoleManagedPolicyArns: ["arn:aws:iam::aws:policy/AmazonSQSFullAccess"], + }, + Effect.gen(function* () { + const queue = yield* JobsQueue; + yield* AWS.SQS.messages(queue, { + batchSize: 10, + maximumBatchingWindowInSeconds: 20, + }).subscribe((stream) => + stream.pipe( + Stream.runForEach((record) => + Effect.logInfo( + `processed SQS message ${record.messageId}: ${record.body ?? ""}`, + ), + ), + ), + ); + }).pipe( + Effect.provide( + Layer.provideMerge( + Layer.mergeAll(Server.SQSQueueEventSource, JobsQueueLive), + Layer.mergeAll( + AWS.SQS.ReceiveMessageLive, + AWS.SQS.DeleteMessageBatchLive, + ), + ), + ), + ), +) {} diff --git a/.repos/alchemy-effect/examples/aws-ecs/src/Task.ts b/.repos/alchemy-effect/examples/aws-ecs/src/Task.ts new file mode 100644 index 00000000000..1d8b080bad6 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ecs/src/Task.ts @@ -0,0 +1,67 @@ +import * as AWS from "alchemy/AWS"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { JobsQueue, JobsQueueLive } from "./JobsQueue.ts"; + +export default class ApiTask extends AWS.ECS.Task()( + "ApiTask", + { + main: import.meta.filename, + cpu: 512, + memory: 1024, + port: 3000, + }, + Effect.gen(function* () { + const queue = yield* JobsQueue; + const sendMessage = yield* AWS.SQS.SendMessage.bind(queue); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + const url = new URL(request.originalUrl); + if (request.method === "GET" && url.pathname === "/") { + return yield* HttpServerResponse.json({ + ok: true, + routes: ["GET /", "GET /enqueue?message=hello"], + }); + } + + if (request.method === "GET" && url.pathname === "/enqueue") { + const message = url.searchParams.get("message") ?? "hello from ECS"; + const body = JSON.stringify({ + message, + enqueuedAt: new Date().toISOString(), + }); + + const result = yield* sendMessage({ + MessageBody: body, + }); + + return yield* HttpServerResponse.json({ + ok: true, + message, + messageId: result.MessageId, + }); + } + + return HttpServerResponse.text("Not found", { status: 404 }); + }).pipe( + Effect.catch(() => + Effect.succeed( + HttpServerResponse.text("Internal server error", { status: 500 }), + ), + ), + ), + }; + }).pipe( + Effect.provide( + Layer.mergeAll( + JobsQueueLive, + AWS.SQS.SendMessageLive, + AWS.SQS.DeleteMessageBatchLive, + ), + ), + ), +) {} diff --git a/.repos/alchemy-effect/examples/aws-ecs/tsconfig.json b/.repos/alchemy-effect/examples/aws-ecs/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-ecs/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-eks/README.md b/.repos/alchemy-effect/examples/aws-eks/README.md new file mode 100644 index 00000000000..bc96e80e28f --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-eks/README.md @@ -0,0 +1,55 @@ +# AWS EKS Auto Mode Example + +This example is fully TypeScript-driven. It provisions: + +- `AWS.EC2.Network` +- `AWS.EKS.AutoCluster` +- `AWS.EKS.AccessEntry` for cluster-admin access +- `AWS.EKS.Addon` for `metrics-server` and `snapshot-controller` +- `AWS.EKS.LoadBalancedWorkload` for deployment + public service composition +- `AWS.EKS.PodIdentityWorkload` for workload identity + deployment composition +- `Kubernetes.Namespace` +- `Kubernetes.Job` + +There is no YAML or `kubectl apply` step. The workloads are declared in +[`alchemy.run.ts`](./alchemy.run.ts) and reconciled by the EKS cluster resource. + +## Commands + +```sh +bun install +bun run --filter aws-eks-example deploy +bun run --filter aws-eks-example destroy +``` + +## Cluster Access + +By default, the example grants cluster-admin access to the IAM principal that is +running the deploy. If your current caller is not a normal IAM user or IAM role, +set this before deploy: + +```sh +export EKS_ADMIN_PRINCIPAL_ARN=arn:aws:iam::123456789012:role/YourAdminRole +``` + +## What Gets Deployed + +The example creates these in-cluster workloads in code: + +- a `demo` namespace +- an `echo-server` workload created with `AWS.EKS.LoadBalancedWorkload` +- a `pod-identity-demo` workload created with `AWS.EKS.PodIdentityWorkload` +- a `cluster-info` job + +The pod identity workload creates the Kubernetes service account, IAM role, +`AWS.EKS.PodIdentityAssociation`, and deployment together. + +## Optional Inspection + +If you want to inspect the cluster manually after deploy, you can still use your +normal kubeconfig flow, for example: + +```sh +aws eks update-kubeconfig --name alchemy-eks-auto-example --region "$AWS_REGION" +kubectl get pods -n demo +``` diff --git a/.repos/alchemy-effect/examples/aws-eks/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-eks/alchemy.run.ts new file mode 100644 index 00000000000..eaa29061a22 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-eks/alchemy.run.ts @@ -0,0 +1,251 @@ +import * as STS from "@distilled.cloud/aws/sts"; +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as EC2 from "alchemy/AWS/EC2"; +import * as EKS from "alchemy/AWS/EKS"; +import * as Kubernetes from "alchemy/Kubernetes"; +import * as Output from "alchemy/Output"; +import * as Config from "effect/Config"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; + +const aws = AWS.providers(); + +const EKS_ADMIN_PRINCIPAL_ARN = Config.string("EKS_ADMIN_PRINCIPAL_ARN").pipe( + Config.option, + Config.map(Option.getOrUndefined), +); + +const clusterName = "alchemy-eks-auto-example"; +const namespace = "demo"; +const serviceAccount = "pod-identity-demo"; + +const toIamPrincipalArn = (arn: string): string | undefined => { + const assumedRole = arn.match( + /^arn:([^:]+):sts::([0-9]{12}):assumed-role\/(.+)\/[^/]+$/, + ); + if (assumedRole) { + const [, partition, accountId, rolePath] = assumedRole; + return `arn:${partition}:iam::${accountId}:role/${rolePath}`; + } + + if (arn.includes(":role/") || arn.includes(":user/")) { + return arn; + } + + return undefined; +}; + +const resolveClusterAdminPrincipalArn = Effect.gen(function* () { + const configured = yield* EKS_ADMIN_PRINCIPAL_ARN; + if (configured) { + return configured; + } + + const caller = yield* STS.getCallerIdentity({}); + const principalArn = + typeof caller.Arn === "string" ? toIamPrincipalArn(caller.Arn) : undefined; + + if (!principalArn) { + return yield* Effect.fail( + new Error( + "Unable to infer an IAM principal ARN for cluster access. Set EKS_ADMIN_PRINCIPAL_ARN before deploy.", + ), + ); + } + + return principalArn; +}); + +export default Alchemy.Stack( + "AwsEksExample", + { + providers: aws, + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const tags = { + Example: "aws-eks", + Surface: "eks", + Mode: "auto", + }; + + const clusterAdminPrincipalArn = yield* resolveClusterAdminPrincipalArn; + + const network = yield* EC2.Network("Network", { + cidrBlock: "10.42.0.0/16", + availabilityZones: 2, + nat: "single", + tags, + }); + + const cluster = yield* EKS.AutoCluster("Cluster", { + clusterName, + network, + tags, + }); + + const clusterAdmin = yield* EKS.AccessEntry("ClusterAdmin", { + clusterName: cluster.cluster.clusterName, + principalArn: clusterAdminPrincipalArn, + accessPolicies: [ + { + policyArn: + "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy", + accessScope: { + type: "cluster", + }, + }, + ], + tags, + }); + + const metricsServer = yield* EKS.Addon("MetricsServer", { + clusterName: cluster.cluster.clusterName, + addonName: "metrics-server", + tags, + }); + + const snapshotController = yield* EKS.Addon("SnapshotController", { + clusterName: cluster.cluster.clusterName, + addonName: "snapshot-controller", + tags, + }); + + const demoNamespace = yield* Kubernetes.Namespace("DemoNamespace", { + cluster: cluster.cluster, + name: namespace, + labels: { + "app.kubernetes.io/part-of": "aws-eks-example", + }, + }); + + const echoServer = yield* EKS.LoadBalancedWorkload("EchoServer", { + cluster: cluster.cluster, + namespace: demoNamespace, + name: "echo-server", + labels: { + "app.kubernetes.io/name": "echo-server", + "app.kubernetes.io/part-of": "aws-eks-example", + }, + replicas: 2, + containers: [ + { + name: "echo-server", + image: "registry.k8s.io/echoserver:1.10", + ports: [ + { + containerPort: 8080, + name: "http", + }, + ], + resources: { + requests: { + cpu: "50m", + memory: "64Mi", + }, + limits: { + cpu: "250m", + memory: "128Mi", + }, + }, + }, + ], + ports: [ + { + name: "http", + port: 80, + targetPort: 8080, + }, + ], + }); + + const podIdentityWorkload = yield* EKS.PodIdentityWorkload( + "PodIdentityDemo", + { + cluster: cluster.cluster, + namespace: demoNamespace, + name: "pod-identity-demo", + serviceAccountName: serviceAccount, + tags, + serviceAccountLabels: { + "app.kubernetes.io/name": serviceAccount, + "app.kubernetes.io/part-of": "aws-eks-example", + }, + labels: { + "app.kubernetes.io/name": "pod-identity-demo", + "app.kubernetes.io/part-of": "aws-eks-example", + }, + containers: [ + { + name: "aws-cli", + image: "public.ecr.aws/aws-cli/aws-cli:2.17.37", + command: ["/bin/sh", "-lc"], + args: [ + [ + "while true; do", + " date;", + " aws sts get-caller-identity;", + " sleep 60;", + "done", + ].join(" "), + ], + resources: { + requests: { + cpu: "50m", + memory: "64Mi", + }, + limits: { + cpu: "250m", + memory: "128Mi", + }, + }, + }, + ], + }, + ); + + const clusterInfoJob = yield* Kubernetes.Job("ClusterInfoJob", { + cluster: cluster.cluster, + namespace: demoNamespace, + name: "cluster-info", + labels: { + "app.kubernetes.io/name": "cluster-info", + "app.kubernetes.io/part-of": "aws-eks-example", + }, + containers: [ + { + name: "cluster-info", + image: "public.ecr.aws/docker/library/busybox:1.36", + command: ["/bin/sh", "-lc"], + args: [ + [ + 'echo "demo workload is running on EKS Auto Mode";', + "nslookup echo-server.demo.svc.cluster.local || true;", + ].join(" "), + ], + }, + ], + }); + + return { + clusterName: cluster.cluster.clusterName, + clusterArn: cluster.cluster.clusterArn, + endpoint: cluster.cluster.endpoint, + adminPrincipalArn: clusterAdmin.principalArn, + namespace: demoNamespace.name, + serviceAccount: podIdentityWorkload.serviceAccount.name, + podIdentityAssociationArn: + podIdentityWorkload.podIdentityAssociation.associationArn, + workloadRoleArn: podIdentityWorkload.roleArn, + metricsServerAddonArn: metricsServer.addonArn, + snapshotControllerAddonArn: snapshotController.addonArn, + echoDeploymentName: echoServer.deployment.name, + echoServiceName: echoServer.service?.name, + podIdentityDeploymentName: podIdentityWorkload.deployment.name, + clusterInfoJobName: clusterInfoJob.name, + accessSummary: Output.interpolate`Granted cluster-admin access on ${cluster.cluster.clusterName} to ${clusterAdmin.principalArn}.`, + workloadSummary: Output.interpolate`Demo workloads are declared in TypeScript and reconciled into namespace ${demoNamespace.name} on ${cluster.cluster.clusterName}.`, + }; + }).pipe(Effect.orDie), +); diff --git a/.repos/alchemy-effect/examples/aws-eks/package.json b/.repos/alchemy-effect/examples/aws-eks/package.json new file mode 100644 index 00000000000..9d3892cc424 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-eks/package.json @@ -0,0 +1,23 @@ +{ + "name": "aws-eks-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-eks" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-eks/tsconfig.json b/.repos/alchemy-effect/examples/aws-eks/tsconfig.json new file mode 100644 index 00000000000..beb7b1841e1 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-eks/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": "../..", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "paths": { + "alchemy": ["../../packages/alchemy/src/index.ts"], + "alchemy/*": ["../../packages/alchemy/src/*"] + } + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-lambda-httpapi/alchemy.run.ts new file mode 100644 index 00000000000..6f61dc9b0af --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/alchemy.run.ts @@ -0,0 +1,87 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Output from "alchemy/Output"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import JobFunction from "./src/JobFunction.ts"; + +const aws = AWS.providers(); +const dashboardRegion = process.env.AWS_REGION ?? "us-west-2"; + +export default Alchemy.Stack( + "JobLambdaHttpApi", + { + providers: Layer.mergeAll( + // Fully configured cloud provider Layers go here: + aws, + ), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const func = yield* JobFunction; + const dashboard = yield* AWS.CloudWatch.Dashboard("JobDashboard", { + DashboardBody: func.functionName.pipe( + Output.map((functionName) => ({ + widgets: [ + { + type: "metric", + x: 0, + y: 0, + width: 12, + height: 6, + properties: { + title: "Lambda Invocations and Errors", + region: dashboardRegion, + stat: "Sum", + period: 300, + metrics: [ + ["AWS/Lambda", "Invocations", "FunctionName", functionName], + ["AWS/Lambda", "Errors", "FunctionName", functionName], + ], + }, + }, + { + type: "metric", + x: 12, + y: 0, + width: 12, + height: 6, + properties: { + title: "Lambda Duration", + region: dashboardRegion, + stat: "Average", + period: 300, + metrics: [ + ["AWS/Lambda", "Duration", "FunctionName", functionName], + ], + }, + }, + ], + })), + ), + }); + const alarm = yield* AWS.CloudWatch.Alarm("JobFunctionErrorsAlarm", { + AlarmDescription: + "Alerts when the example Lambda function reports errors.", + MetricName: "Errors", + Namespace: "AWS/Lambda", + Statistic: "Sum", + Period: 300, + EvaluationPeriods: 1, + Threshold: 1, + ComparisonOperator: "GreaterThanOrEqualToThreshold", + TreatMissingData: "notBreaching", + Dimensions: [ + { + Name: "FunctionName", + Value: func.functionName, + }, + ], + }); + return { + url: Output.interpolate`${func.functionUrl}?jobId=foo`, + dashboardName: dashboard.dashboardName, + alarmName: alarm.alarmName, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/package.json b/.repos/alchemy-effect/examples/aws-lambda-httpapi/package.json new file mode 100644 index 00000000000..c7f539b5dfd --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/package.json @@ -0,0 +1,23 @@ +{ + "name": "aws-lambda-httpapi", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-lambda-httpapi" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/Job.ts b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/Job.ts new file mode 100644 index 00000000000..5a5424a5c1e --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/Job.ts @@ -0,0 +1,13 @@ +import * as S from "effect/Schema"; + +export type JobId = S.Schema.Type; +export const JobId = S.String.annotate({ + description: "The ID of the job", +}); + +export class Job extends S.Class("Job")({ + id: JobId, + content: S.String, +}) {} + +export const decodeJob = S.decodeEffect(Job); diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobApi.ts b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobApi.ts new file mode 100644 index 00000000000..d2b4caf408e --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobApi.ts @@ -0,0 +1,106 @@ +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import * as HttpApi from "effect/unstable/httpapi/HttpApi"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; +import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint"; +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup"; +import { Job, JobId } from "./Job.ts"; +import { JobNotifications, NotifyJobError } from "./JobNotifications.ts"; +import { GetJobError, JobStorage, PutJobError } from "./JobStorage.ts"; + +export const getJob = HttpApiEndpoint.get("getJob", "/", { + success: Job, + query: { + jobId: JobId.pipe(Schema.optional), + }, +}); + +export const createJob = HttpApiEndpoint.post("createJob", "/", { + success: JobId, + payload: Schema.Struct({ + content: Schema.String, + }), +}); + +export const JobApi = HttpApi.make("JobApi").add( + HttpApiGroup.make("Jobs").add(getJob, createJob), +); + +export const JobApiLive = HttpApiBuilder.layer(JobApi).pipe( + Layer.provide( + HttpApiBuilder.group(JobApi, "Jobs", (handlers) => + Effect.gen(function* () { + const jobService = yield* JobStorage; + const notifications = yield* JobNotifications; + + return handlers + .handle( + "getJob", + Effect.fn(function* (req) { + if (!req.query.jobId) { + return HttpServerResponse.text("Job ID is required", { + status: 400, + }); + } + const job = yield* jobService.getJob(req.query.jobId).pipe( + Effect.catchTag("GetJobError", (error) => + Effect.succeed( + HttpServerResponse.text(error.message, { + status: 500, + }), + ), + ), + ); + if (job instanceof GetJobError) { + return HttpServerResponse.text(job.message, { + status: 500, + }); + } + if (!job) { + return HttpServerResponse.text("Job not found", { + status: 404, + }); + } + return job!; + }), + ) + .handle( + "createJob", + Effect.fn(function* (req) { + const jobId = crypto.randomUUID(); + const job = yield* jobService + .putJob({ + id: jobId, + content: req.payload.content, + }) + .pipe( + Effect.catchTag("PutJobError", (error) => + Effect.succeed(error), + ), + ); + if (job instanceof PutJobError) { + return HttpServerResponse.text(job.message, { + status: 500, + }); + } + const notificationResult = yield* notifications + .notifyJobCreated(job) + .pipe( + Effect.catchTag("NotifyJobError", (error) => + Effect.succeed(error), + ), + ); + if (notificationResult instanceof NotifyJobError) { + return HttpServerResponse.text(notificationResult.message, { + status: 500, + }); + } + return job.id; + }), + ); + }), + ), + ), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobFunction.ts b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobFunction.ts new file mode 100644 index 00000000000..82d2f1f5363 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobFunction.ts @@ -0,0 +1,29 @@ +import * as AWS from "alchemy/AWS"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Etag from "effect/unstable/http/Etag"; +import * as HttpPlatform from "effect/unstable/http/HttpPlatform"; +import * as HttpRouter from "effect/unstable/http/HttpRouter"; +import { JobApiLive } from "./JobApi.ts"; +import { JobNotificationsSNS } from "./JobNotifications.ts"; +import { JobStorageDynamoDB } from "./JobStorage.ts"; + +export default class JobFunction extends AWS.Lambda.Function()( + "JobFunction", + { + main: import.meta.filename, + url: true, + }, + HttpRouter.toHttpEffect(JobApiLive).pipe( + Effect.map((fetch) => ({ fetch })), + Effect.provide( + Layer.mergeAll( + JobStorageDynamoDB, + JobNotificationsSNS, + // TODO(sam): these should be provided to us automatically + HttpPlatform.layer, + Etag.layer, + ), + ), + ), +) {} diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobNotifications.ts b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobNotifications.ts new file mode 100644 index 00000000000..897257dd564 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobNotifications.ts @@ -0,0 +1,89 @@ +import * as AWS from "alchemy/AWS"; +import * as Context from "effect/Context"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import type { Job } from "./Job.ts"; + +export class NotifyJobError extends Data.TaggedError("NotifyJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +type JobNotification = { + type: "job.created"; + job: Job; +}; + +export class JobNotifications extends Context.Service< + JobNotifications, + { + notifyJobCreated(job: Job): Effect.Effect; + } +>()("JobNotifications") {} + +export const JobNotificationsSNS = Layer.provideMerge( + Layer.effect( + JobNotifications, + Effect.gen(function* () { + const topic = yield* AWS.SNS.Topic("JobNotificationsTopic", { + attributes: { + DisplayName: "job-notifications", + }, + }); + + const publish = yield* AWS.SNS.Publish.bind(topic); + + yield* AWS.SNS.notifications(topic).subscribe((stream) => + stream.pipe( + Stream.mapEffect((notification) => + Effect.try({ + try: () => JSON.parse(notification.Message) as JobNotification, + catch: (cause) => + new NotifyJobError({ + message: "Failed to parse SNS job notification", + cause, + }), + }).pipe( + Effect.flatMap((payload) => + Effect.logInfo( + `Job notification received: ${payload.type} (${payload.job.id})`, + ), + ), + // Keep the example resilient to malformed demo messages. + Effect.catchTag("NotifyJobError", (error) => + Effect.logWarning(error.message), + ), + ), + ), + Stream.runDrain, + ), + ); + + const notifyJobCreated = (job: Job) => + publish({ + Subject: "JobCreated", + Message: JSON.stringify({ + type: "job.created", + job, + } satisfies JobNotification), + }).pipe( + Effect.asVoid, + Effect.mapError( + (cause) => + new NotifyJobError({ + message: `Failed to publish job notification for "${job.id}"`, + cause, + }), + ), + ); + + return JobNotifications.of({ + notifyJobCreated, + }); + }), + ), + Layer.mergeAll(AWS.Lambda.TopicEventSource, AWS.SNS.PublishLive), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobStorage.ts b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobStorage.ts new file mode 100644 index 00000000000..15d42ae9fde --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/src/JobStorage.ts @@ -0,0 +1,230 @@ +import * as DynamoDB from "alchemy/AWS/DynamoDB"; +import * as Lambda from "alchemy/AWS/Lambda"; +import * as S3 from "alchemy/AWS/S3"; +import * as SQS from "alchemy/AWS/SQS"; +import * as RemovalPolicy from "alchemy/RemovalPolicy"; +import { Stack } from "alchemy/Stack"; +import * as Console from "effect/Console"; +import * as Context from "effect/Context"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import type { Job } from "./Job.ts"; + +export class PutJobError extends Data.TaggedError("PutJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class GetJobError extends Data.TaggedError("GetJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class JobStorage extends Context.Service< + JobStorage, + { + putJob(job: Job): Effect.Effect; + getJob(jobId: string): Effect.Effect; + } +>()("JobStorage") {} + +export const JobStorageDynamoDB = Layer.provideMerge( + Layer.effect( + JobStorage, + Effect.gen(function* () { + const stack = yield* Stack; + const table = yield* DynamoDB.Table("JobsTable", { + partitionKey: "id", + attributes: { + id: "S", + }, + }); + const queue = yield* SQS.Queue("JobsQueue").pipe( + RemovalPolicy.retain(stack.stage === "prod"), + ); + + const getItem = yield* DynamoDB.GetItem.bind(table); + const putItem = yield* DynamoDB.PutItem.bind(table); + const sink = yield* SQS.QueueSink.bind(queue); + + yield* DynamoDB.stream(table, { + streamViewType: "NEW_AND_OLD_IMAGES", + startingPosition: "LATEST", + batchSize: 10, + }).process((stream) => + stream.pipe( + Stream.map((record) => + JSON.stringify({ + eventName: record.eventName, + keys: record.dynamodb.Keys, + newImage: record.dynamodb.NewImage, + oldImage: record.dynamodb.OldImage, + }), + ), + Stream.run(sink), + ), + ); + + const putJob = (job: Job) => + putItem({ + Item: { + id: { S: job.id }, + content: { S: job.content }, + }, + }).pipe( + Effect.map(() => job), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new PutJobError({ + message: `Failed to store job "${job.id}": ${cause}`, + cause, + }), + ), + ), + ); + + const getJob = (jobId: string) => + getItem({ + Key: { + id: { S: jobId }, + }, + }).pipe( + Effect.flatMap((item) => + item.Item + ? Effect.try({ + try: () => + ({ + id: item.Item?.id?.S ?? jobId, + content: item.Item?.content?.S ?? "", + }) as Job, + catch: (cause) => + new GetJobError({ + message: `Failed to parse job "${jobId}": ${cause}`, + cause, + }), + }) + : Effect.succeed(undefined), + ), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new GetJobError({ + message: `Failed to load job "${jobId}": ${cause}`, + cause, + }), + ), + ), + ); + + return JobStorage.of({ + putJob, + getJob, + }); + }), + ), + Layer.mergeAll(Lambda.TableEventSource, SQS.QueueSinkLive).pipe( + Layer.provideMerge( + Layer.mergeAll( + DynamoDB.GetItemLive, + DynamoDB.PutItemLive, + SQS.SendMessageBatchLive, + ), + ), + ), +); + +export const JobStorageS3 = Layer.provideMerge( + Layer.effect( + JobStorage, + Effect.gen(function* () { + const stack = yield* Stack; + const bucket = yield* S3.Bucket("JobsBucket"); + const queue = yield* SQS.Queue("JobsQueue").pipe( + RemovalPolicy.retain(stack.stage === "prod"), + ); + + const getObject = yield* S3.GetObject.bind(bucket); + const putObject = yield* S3.PutObject.bind(bucket); + const sink = yield* SQS.QueueSink.bind(queue); + + const putJob = (job: Job) => + putObject({ + Key: job.id, + Body: JSON.stringify(job), + }).pipe( + Effect.map(() => job), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new PutJobError({ + message: `Failed to store job "${job.id}": ${cause}`, + cause, + }), + ), + ), + ); + + const getJob = (jobId: string) => + getObject({ + Key: jobId, + }).pipe( + Effect.catchTag("NoSuchKey", () => Effect.succeed(undefined)), + Effect.flatMap( + (item) => + item?.Body?.pipe( + Stream.decodeText, + Stream.mkString, + Effect.flatMap((body) => + Effect.try({ + try: () => JSON.parse(body) as Job, + catch: (cause) => + new GetJobError({ + message: `Failed to parse job "${jobId}": ${cause}`, + cause, + }), + }), + ), + ) ?? Effect.succeed(undefined), + ), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new GetJobError({ + message: `Failed to load job "${jobId}": ${cause}`, + cause, + }), + ), + ), + ); + + yield* S3.notifications(bucket).subscribe((stream) => + stream.pipe( + Stream.flatMap((item) => + Stream.fromEffect(getJob(item.key).pipe(Effect.orDie)), + ), + Stream.filter((job): job is Job => job !== undefined), + Stream.map((job) => JSON.stringify(job)), + Stream.run(sink), + ), + ); + + return JobStorage.of({ + putJob, + getJob, + }); + }), + ), + Layer.mergeAll(Lambda.BucketEventSource, SQS.QueueSinkLive).pipe( + Layer.provideMerge( + Layer.mergeAll( + S3.GetObjectLive, + S3.PutObjectLive, + SQS.SendMessageBatchLive, + ), + ), + ), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-httpapi/tsconfig.json b/.repos/alchemy-effect/examples/aws-lambda-httpapi/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-httpapi/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-lambda-rpc/alchemy.run.ts new file mode 100644 index 00000000000..a19bc4b39ce --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/alchemy.run.ts @@ -0,0 +1,87 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Output from "alchemy/Output"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import JobFunction from "./src/JobFunction.ts"; + +const aws = AWS.providers(); +const dashboardRegion = process.env.AWS_REGION ?? "us-west-2"; + +export default Alchemy.Stack( + "JobLambdaRpc", + { + providers: Layer.mergeAll( + // Fully configured cloud provider Layers go here: + aws, + ), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const func = yield* JobFunction; + const dashboard = yield* AWS.CloudWatch.Dashboard("JobDashboard", { + DashboardBody: func.functionName.pipe( + Output.map((functionName) => ({ + widgets: [ + { + type: "metric", + x: 0, + y: 0, + width: 12, + height: 6, + properties: { + title: "Lambda Invocations and Errors", + region: dashboardRegion, + stat: "Sum", + period: 300, + metrics: [ + ["AWS/Lambda", "Invocations", "FunctionName", functionName], + ["AWS/Lambda", "Errors", "FunctionName", functionName], + ], + }, + }, + { + type: "metric", + x: 12, + y: 0, + width: 12, + height: 6, + properties: { + title: "Lambda Duration", + region: dashboardRegion, + stat: "Average", + period: 300, + metrics: [ + ["AWS/Lambda", "Duration", "FunctionName", functionName], + ], + }, + }, + ], + })), + ), + }); + const alarm = yield* AWS.CloudWatch.Alarm("JobFunctionErrorsAlarm", { + AlarmDescription: + "Alerts when the example Lambda function reports errors.", + MetricName: "Errors", + Namespace: "AWS/Lambda", + Statistic: "Sum", + Period: 300, + EvaluationPeriods: 1, + Threshold: 1, + ComparisonOperator: "GreaterThanOrEqualToThreshold", + TreatMissingData: "notBreaching", + Dimensions: [ + { + Name: "FunctionName", + Value: func.functionName, + }, + ], + }); + return { + url: func.functionUrl, + dashboardName: dashboard.dashboardName, + alarmName: alarm.alarmName, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/package.json b/.repos/alchemy-effect/examples/aws-lambda-rpc/package.json new file mode 100644 index 00000000000..51a641345ec --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/package.json @@ -0,0 +1,23 @@ +{ + "name": "aws-lambda-rpc", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-lambda-rpc" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/src/Job.ts b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/Job.ts new file mode 100644 index 00000000000..5a5424a5c1e --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/Job.ts @@ -0,0 +1,13 @@ +import * as S from "effect/Schema"; + +export type JobId = S.Schema.Type; +export const JobId = S.String.annotate({ + description: "The ID of the job", +}); + +export class Job extends S.Class("Job")({ + id: JobId, + content: S.String, +}) {} + +export const decodeJob = S.decodeEffect(Job); diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobFunction.ts b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobFunction.ts new file mode 100644 index 00000000000..352f471f93a --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobFunction.ts @@ -0,0 +1,18 @@ +import * as AWS from "alchemy/AWS"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { JobNotificationsSNS } from "./JobNotifications.ts"; +import { JobRpcHttpEffect } from "./JobRpcApi.ts"; +import { JobStorageDynamoDB } from "./JobStorage.ts"; + +export default class JobFunction extends AWS.Lambda.Function()( + "JobFunction", + { + main: import.meta.filename, + url: true, + }, + JobRpcHttpEffect.pipe( + Effect.map((fetch) => ({ fetch })), + Effect.provide(Layer.mergeAll(JobStorageDynamoDB, JobNotificationsSNS)), + ), +) {} diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobNotifications.ts b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobNotifications.ts new file mode 100644 index 00000000000..897257dd564 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobNotifications.ts @@ -0,0 +1,89 @@ +import * as AWS from "alchemy/AWS"; +import * as Context from "effect/Context"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import type { Job } from "./Job.ts"; + +export class NotifyJobError extends Data.TaggedError("NotifyJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +type JobNotification = { + type: "job.created"; + job: Job; +}; + +export class JobNotifications extends Context.Service< + JobNotifications, + { + notifyJobCreated(job: Job): Effect.Effect; + } +>()("JobNotifications") {} + +export const JobNotificationsSNS = Layer.provideMerge( + Layer.effect( + JobNotifications, + Effect.gen(function* () { + const topic = yield* AWS.SNS.Topic("JobNotificationsTopic", { + attributes: { + DisplayName: "job-notifications", + }, + }); + + const publish = yield* AWS.SNS.Publish.bind(topic); + + yield* AWS.SNS.notifications(topic).subscribe((stream) => + stream.pipe( + Stream.mapEffect((notification) => + Effect.try({ + try: () => JSON.parse(notification.Message) as JobNotification, + catch: (cause) => + new NotifyJobError({ + message: "Failed to parse SNS job notification", + cause, + }), + }).pipe( + Effect.flatMap((payload) => + Effect.logInfo( + `Job notification received: ${payload.type} (${payload.job.id})`, + ), + ), + // Keep the example resilient to malformed demo messages. + Effect.catchTag("NotifyJobError", (error) => + Effect.logWarning(error.message), + ), + ), + ), + Stream.runDrain, + ), + ); + + const notifyJobCreated = (job: Job) => + publish({ + Subject: "JobCreated", + Message: JSON.stringify({ + type: "job.created", + job, + } satisfies JobNotification), + }).pipe( + Effect.asVoid, + Effect.mapError( + (cause) => + new NotifyJobError({ + message: `Failed to publish job notification for "${job.id}"`, + cause, + }), + ), + ); + + return JobNotifications.of({ + notifyJobCreated, + }); + }), + ), + Layer.mergeAll(AWS.Lambda.TopicEventSource, AWS.SNS.PublishLive), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobRpcApi.ts b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobRpcApi.ts new file mode 100644 index 00000000000..2c86d1b4db2 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobRpcApi.ts @@ -0,0 +1,86 @@ +import { Effect, Layer, Schema } from "effect"; +import { + Rpc, + RpcGroup, + RpcSerialization, + RpcServer, +} from "effect/unstable/rpc"; +import { Job, JobId } from "./Job.ts"; +import { JobNotifications } from "./JobNotifications.ts"; +import { JobStorage } from "./JobStorage.ts"; + +export class JobNotFound extends Schema.TaggedClass()( + "JobNotFound", + { jobId: JobId }, +) {} + +export class GetJobFailed extends Schema.TaggedClass()( + "GetJobFailed", + { message: Schema.String }, +) {} + +export class PutJobFailed extends Schema.TaggedClass()( + "PutJobFailed", + { message: Schema.String }, +) {} + +const getJob = Rpc.make("getJob", { + success: Job, + error: Schema.Union([JobNotFound, GetJobFailed]), + payload: { + jobId: JobId, + }, +}); + +const createJob = Rpc.make("createJob", { + success: JobId, + error: PutJobFailed, + payload: { + content: Schema.String, + }, +}); + +export class JobRpcs extends RpcGroup.make(getJob, createJob) {} + +export const JobRpcsLive = JobRpcs.toLayer( + Effect.gen(function* () { + const jobService = yield* JobStorage; + const notifications = yield* JobNotifications; + + return { + getJob: ({ jobId }) => + jobService.getJob(jobId).pipe( + Effect.mapError( + (error) => + new GetJobFailed({ + message: error.message, + }), + ), + Effect.flatMap((job) => + job ? Effect.succeed(job) : Effect.fail(new JobNotFound({ jobId })), + ), + ), + createJob: ({ content }) => + Effect.gen(function* () { + const jobId = crypto.randomUUID(); + const job = yield* jobService.putJob({ + id: jobId, + content, + }); + yield* notifications.notifyJobCreated(job); + return job.id; + }).pipe( + Effect.mapError( + (error) => + new PutJobFailed({ + message: error.message, + }), + ), + ), + }; + }), +); + +export const JobRpcHttpEffect = RpcServer.toHttpEffect(JobRpcs).pipe( + Effect.provide(Layer.mergeAll(JobRpcsLive, RpcSerialization.layerJson)), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobStorage.ts b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobStorage.ts new file mode 100644 index 00000000000..15d42ae9fde --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/src/JobStorage.ts @@ -0,0 +1,230 @@ +import * as DynamoDB from "alchemy/AWS/DynamoDB"; +import * as Lambda from "alchemy/AWS/Lambda"; +import * as S3 from "alchemy/AWS/S3"; +import * as SQS from "alchemy/AWS/SQS"; +import * as RemovalPolicy from "alchemy/RemovalPolicy"; +import { Stack } from "alchemy/Stack"; +import * as Console from "effect/Console"; +import * as Context from "effect/Context"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import type { Job } from "./Job.ts"; + +export class PutJobError extends Data.TaggedError("PutJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class GetJobError extends Data.TaggedError("GetJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class JobStorage extends Context.Service< + JobStorage, + { + putJob(job: Job): Effect.Effect; + getJob(jobId: string): Effect.Effect; + } +>()("JobStorage") {} + +export const JobStorageDynamoDB = Layer.provideMerge( + Layer.effect( + JobStorage, + Effect.gen(function* () { + const stack = yield* Stack; + const table = yield* DynamoDB.Table("JobsTable", { + partitionKey: "id", + attributes: { + id: "S", + }, + }); + const queue = yield* SQS.Queue("JobsQueue").pipe( + RemovalPolicy.retain(stack.stage === "prod"), + ); + + const getItem = yield* DynamoDB.GetItem.bind(table); + const putItem = yield* DynamoDB.PutItem.bind(table); + const sink = yield* SQS.QueueSink.bind(queue); + + yield* DynamoDB.stream(table, { + streamViewType: "NEW_AND_OLD_IMAGES", + startingPosition: "LATEST", + batchSize: 10, + }).process((stream) => + stream.pipe( + Stream.map((record) => + JSON.stringify({ + eventName: record.eventName, + keys: record.dynamodb.Keys, + newImage: record.dynamodb.NewImage, + oldImage: record.dynamodb.OldImage, + }), + ), + Stream.run(sink), + ), + ); + + const putJob = (job: Job) => + putItem({ + Item: { + id: { S: job.id }, + content: { S: job.content }, + }, + }).pipe( + Effect.map(() => job), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new PutJobError({ + message: `Failed to store job "${job.id}": ${cause}`, + cause, + }), + ), + ), + ); + + const getJob = (jobId: string) => + getItem({ + Key: { + id: { S: jobId }, + }, + }).pipe( + Effect.flatMap((item) => + item.Item + ? Effect.try({ + try: () => + ({ + id: item.Item?.id?.S ?? jobId, + content: item.Item?.content?.S ?? "", + }) as Job, + catch: (cause) => + new GetJobError({ + message: `Failed to parse job "${jobId}": ${cause}`, + cause, + }), + }) + : Effect.succeed(undefined), + ), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new GetJobError({ + message: `Failed to load job "${jobId}": ${cause}`, + cause, + }), + ), + ), + ); + + return JobStorage.of({ + putJob, + getJob, + }); + }), + ), + Layer.mergeAll(Lambda.TableEventSource, SQS.QueueSinkLive).pipe( + Layer.provideMerge( + Layer.mergeAll( + DynamoDB.GetItemLive, + DynamoDB.PutItemLive, + SQS.SendMessageBatchLive, + ), + ), + ), +); + +export const JobStorageS3 = Layer.provideMerge( + Layer.effect( + JobStorage, + Effect.gen(function* () { + const stack = yield* Stack; + const bucket = yield* S3.Bucket("JobsBucket"); + const queue = yield* SQS.Queue("JobsQueue").pipe( + RemovalPolicy.retain(stack.stage === "prod"), + ); + + const getObject = yield* S3.GetObject.bind(bucket); + const putObject = yield* S3.PutObject.bind(bucket); + const sink = yield* SQS.QueueSink.bind(queue); + + const putJob = (job: Job) => + putObject({ + Key: job.id, + Body: JSON.stringify(job), + }).pipe( + Effect.map(() => job), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new PutJobError({ + message: `Failed to store job "${job.id}": ${cause}`, + cause, + }), + ), + ), + ); + + const getJob = (jobId: string) => + getObject({ + Key: jobId, + }).pipe( + Effect.catchTag("NoSuchKey", () => Effect.succeed(undefined)), + Effect.flatMap( + (item) => + item?.Body?.pipe( + Stream.decodeText, + Stream.mkString, + Effect.flatMap((body) => + Effect.try({ + try: () => JSON.parse(body) as Job, + catch: (cause) => + new GetJobError({ + message: `Failed to parse job "${jobId}": ${cause}`, + cause, + }), + }), + ), + ) ?? Effect.succeed(undefined), + ), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new GetJobError({ + message: `Failed to load job "${jobId}": ${cause}`, + cause, + }), + ), + ), + ); + + yield* S3.notifications(bucket).subscribe((stream) => + stream.pipe( + Stream.flatMap((item) => + Stream.fromEffect(getJob(item.key).pipe(Effect.orDie)), + ), + Stream.filter((job): job is Job => job !== undefined), + Stream.map((job) => JSON.stringify(job)), + Stream.run(sink), + ), + ); + + return JobStorage.of({ + putJob, + getJob, + }); + }), + ), + Layer.mergeAll(Lambda.BucketEventSource, SQS.QueueSinkLive).pipe( + Layer.provideMerge( + Layer.mergeAll( + S3.GetObjectLive, + S3.PutObjectLive, + SQS.SendMessageBatchLive, + ), + ), + ), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda-rpc/tsconfig.json b/.repos/alchemy-effect/examples/aws-lambda-rpc/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda-rpc/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-lambda/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-lambda/alchemy.run.ts new file mode 100644 index 00000000000..5f63ddcc845 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/alchemy.run.ts @@ -0,0 +1,86 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Output from "alchemy/Output"; +import * as Effect from "effect/Effect"; +import JobFunction from "./src/JobFunction.ts"; + +// AWS.providers() already provides AWSEnvironment from the SSO profile +// named by $AWS_PROFILE (defaults to "default"). To pin a different +// profile per stage, wrap with `Layer.provide(AWS.makeEnvironment({...}))`. +const aws = AWS.providers(); +const dashboardRegion = process.env.AWS_REGION ?? "us-west-2"; + +export default Alchemy.Stack( + "JobLambda", + { + providers: aws, + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const func = yield* JobFunction; + const dashboard = yield* AWS.CloudWatch.Dashboard("JobDashboard", { + DashboardBody: func.functionName.pipe( + Output.map((functionName) => ({ + widgets: [ + { + type: "metric", + x: 0, + y: 0, + width: 12, + height: 6, + properties: { + title: "Lambda Invocations and Errors", + region: dashboardRegion, + stat: "Sum", + period: 300, + metrics: [ + ["AWS/Lambda", "Invocations", "FunctionName", functionName], + ["AWS/Lambda", "Errors", "FunctionName", functionName], + ], + }, + }, + { + type: "metric", + x: 12, + y: 0, + width: 12, + height: 6, + properties: { + title: "Lambda Duration", + region: dashboardRegion, + stat: "Average", + period: 300, + metrics: [ + ["AWS/Lambda", "Duration", "FunctionName", functionName], + ], + }, + }, + ], + })), + ), + }); + const alarm = yield* AWS.CloudWatch.Alarm("JobFunctionErrorsAlarm", { + AlarmDescription: + "Alerts when the example Lambda function reports errors.", + MetricName: "Errors", + Namespace: "AWS/Lambda", + Statistic: "Sum", + Period: 300, + EvaluationPeriods: 1, + Threshold: 1, + ComparisonOperator: "GreaterThanOrEqualToThreshold", + TreatMissingData: "notBreaching", + Dimensions: [ + { + Name: "FunctionName", + Value: func.functionName, + }, + ], + }); + return { + url: Output.interpolate`${func.functionUrl}?jobId=foo`, + dashboardName: dashboard.dashboardName, + alarmName: alarm.alarmName, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda/package.json b/.repos/alchemy-effect/examples/aws-lambda/package.json new file mode 100644 index 00000000000..b56343c2247 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/package.json @@ -0,0 +1,24 @@ +{ + "name": "aws-lambda", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-lambda" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "test": "bun test" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-lambda/src/Job.ts b/.repos/alchemy-effect/examples/aws-lambda/src/Job.ts new file mode 100644 index 00000000000..5a5424a5c1e --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/src/Job.ts @@ -0,0 +1,13 @@ +import * as S from "effect/Schema"; + +export type JobId = S.Schema.Type; +export const JobId = S.String.annotate({ + description: "The ID of the job", +}); + +export class Job extends S.Class("Job")({ + id: JobId, + content: S.String, +}) {} + +export const decodeJob = S.decodeEffect(Job); diff --git a/.repos/alchemy-effect/examples/aws-lambda/src/JobFunction.ts b/.repos/alchemy-effect/examples/aws-lambda/src/JobFunction.ts new file mode 100644 index 00000000000..e835ec03120 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/src/JobFunction.ts @@ -0,0 +1,119 @@ +import * as AWS from "alchemy/AWS"; +import { Stack } from "alchemy/Stack"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { + JobNotifications, + JobNotificationsSNS, + NotifyJobError, +} from "./JobNotifications.ts"; +import { + GetJobError, + JobStorage, + JobStorageDynamoDB, + PutJobError, +} from "./JobStorage.ts"; + +export default class JobFunction extends AWS.Lambda.Function()( + "JobFunction", + Stack.useSync((stack) => ({ + main: import.meta.filename, + memory: stack.stage === "prod" ? 1024 : 512, + url: true, + })), + Effect.gen(function* () { + const jobStorage = yield* JobStorage; + const notifications = yield* JobNotifications; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.originalUrl); + + if (request.method === "GET" && url.pathname === "/") { + const jobId = url.searchParams.get("jobId"); + if (!jobId) { + return HttpServerResponse.text("Job ID is required", { + status: 400, + }); + } + + const job = yield* jobStorage.getJob(jobId).pipe( + Effect.match({ + onFailure: (error) => error, + onSuccess: (job) => job, + }), + ); + + if (job instanceof GetJobError) { + return HttpServerResponse.text(job.message, { status: 500 }); + } + + if (!job) { + return HttpServerResponse.text("Job not found", { status: 404 }); + } + + return yield* HttpServerResponse.json(job); + } + + if (request.method === "POST" && url.pathname === "/") { + const content = yield* request.text; + if (!content) { + return HttpServerResponse.text("Job content is required", { + status: 400, + }); + } + + const job = yield* jobStorage + .putJob({ + id: crypto.randomUUID(), + content, + }) + .pipe( + Effect.match({ + onFailure: (error) => error, + onSuccess: (job) => job, + }), + ); + + if (job instanceof PutJobError) { + return HttpServerResponse.text(job.message, { status: 500 }); + } + + const notificationResult = yield* notifications + .notifyJobCreated(job) + .pipe( + Effect.match({ + onFailure: (error) => error, + onSuccess: () => undefined, + }), + ); + + if (notificationResult instanceof NotifyJobError) { + return HttpServerResponse.text(notificationResult.message, { + status: 500, + }); + } + + return yield* HttpServerResponse.json( + { jobId: job.id }, + { status: 201 }, + ); + } + + return HttpServerResponse.text("Not found", { status: 404 }); + }), + }; + }).pipe( + Effect.provide( + Layer.mergeAll( + // Services go here + JobStorageDynamoDB, + JobNotificationsSNS, + // JobStorageS3, + ), + ), + ), +) {} diff --git a/.repos/alchemy-effect/examples/aws-lambda/src/JobNotifications.ts b/.repos/alchemy-effect/examples/aws-lambda/src/JobNotifications.ts new file mode 100644 index 00000000000..28a8959cdee --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/src/JobNotifications.ts @@ -0,0 +1,90 @@ +import * as AWS from "alchemy/AWS"; +import * as Context from "effect/Context"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import type { Job } from "./Job.ts"; + +export class NotifyJobError extends Data.TaggedError("NotifyJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +type JobNotification = { + type: "job.created"; + job: Job; +}; + +export class JobNotifications extends Context.Service< + JobNotifications, + { + notifyJobCreated(job: Job): Effect.Effect; + } +>()("JobNotifications") {} + +export const JobNotificationsSNS = Layer.effect( + JobNotifications, + Effect.gen(function* () { + const topic = yield* AWS.SNS.Topic("JobNotificationsTopic", { + attributes: { + DisplayName: "job-notifications", + }, + }); + + const publish = yield* AWS.SNS.Publish.bind(topic); + + yield* AWS.SNS.notifications(topic).subscribe((stream) => + stream.pipe( + Stream.mapEffect((notification) => + Effect.try({ + try: () => JSON.parse(notification.Message) as JobNotification, + catch: (cause) => + new NotifyJobError({ + message: "Failed to parse SNS job notification", + cause, + }), + }).pipe( + Effect.flatMap((payload) => + Effect.logInfo( + `Job notification received: ${payload.type} (${payload.job.id})`, + ), + ), + // Keep the example resilient to malformed demo messages. + Effect.catchTag("NotifyJobError", (error) => + Effect.logWarning(error.message), + ), + ), + ), + Stream.runDrain, + ), + ); + + const notifyJobCreated = (job: Job) => + publish({ + Subject: "JobCreated", + Message: JSON.stringify({ + type: "job.created", + job, + } satisfies JobNotification), + }).pipe( + Effect.asVoid, + Effect.mapError( + (cause) => + new NotifyJobError({ + message: `Failed to publish job notification for "${job.id}"`, + cause, + }), + ), + ); + + return JobNotifications.of({ + notifyJobCreated, + }); + }), +).pipe( + Layer.provideMerge( + Layer.mergeAll(AWS.Lambda.TopicEventSource, AWS.SNS.PublishLive), + ), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda/src/JobStorage.ts b/.repos/alchemy-effect/examples/aws-lambda/src/JobStorage.ts new file mode 100644 index 00000000000..15d42ae9fde --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/src/JobStorage.ts @@ -0,0 +1,230 @@ +import * as DynamoDB from "alchemy/AWS/DynamoDB"; +import * as Lambda from "alchemy/AWS/Lambda"; +import * as S3 from "alchemy/AWS/S3"; +import * as SQS from "alchemy/AWS/SQS"; +import * as RemovalPolicy from "alchemy/RemovalPolicy"; +import { Stack } from "alchemy/Stack"; +import * as Console from "effect/Console"; +import * as Context from "effect/Context"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +import type { Job } from "./Job.ts"; + +export class PutJobError extends Data.TaggedError("PutJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class GetJobError extends Data.TaggedError("GetJobError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class JobStorage extends Context.Service< + JobStorage, + { + putJob(job: Job): Effect.Effect; + getJob(jobId: string): Effect.Effect; + } +>()("JobStorage") {} + +export const JobStorageDynamoDB = Layer.provideMerge( + Layer.effect( + JobStorage, + Effect.gen(function* () { + const stack = yield* Stack; + const table = yield* DynamoDB.Table("JobsTable", { + partitionKey: "id", + attributes: { + id: "S", + }, + }); + const queue = yield* SQS.Queue("JobsQueue").pipe( + RemovalPolicy.retain(stack.stage === "prod"), + ); + + const getItem = yield* DynamoDB.GetItem.bind(table); + const putItem = yield* DynamoDB.PutItem.bind(table); + const sink = yield* SQS.QueueSink.bind(queue); + + yield* DynamoDB.stream(table, { + streamViewType: "NEW_AND_OLD_IMAGES", + startingPosition: "LATEST", + batchSize: 10, + }).process((stream) => + stream.pipe( + Stream.map((record) => + JSON.stringify({ + eventName: record.eventName, + keys: record.dynamodb.Keys, + newImage: record.dynamodb.NewImage, + oldImage: record.dynamodb.OldImage, + }), + ), + Stream.run(sink), + ), + ); + + const putJob = (job: Job) => + putItem({ + Item: { + id: { S: job.id }, + content: { S: job.content }, + }, + }).pipe( + Effect.map(() => job), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new PutJobError({ + message: `Failed to store job "${job.id}": ${cause}`, + cause, + }), + ), + ), + ); + + const getJob = (jobId: string) => + getItem({ + Key: { + id: { S: jobId }, + }, + }).pipe( + Effect.flatMap((item) => + item.Item + ? Effect.try({ + try: () => + ({ + id: item.Item?.id?.S ?? jobId, + content: item.Item?.content?.S ?? "", + }) as Job, + catch: (cause) => + new GetJobError({ + message: `Failed to parse job "${jobId}": ${cause}`, + cause, + }), + }) + : Effect.succeed(undefined), + ), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new GetJobError({ + message: `Failed to load job "${jobId}": ${cause}`, + cause, + }), + ), + ), + ); + + return JobStorage.of({ + putJob, + getJob, + }); + }), + ), + Layer.mergeAll(Lambda.TableEventSource, SQS.QueueSinkLive).pipe( + Layer.provideMerge( + Layer.mergeAll( + DynamoDB.GetItemLive, + DynamoDB.PutItemLive, + SQS.SendMessageBatchLive, + ), + ), + ), +); + +export const JobStorageS3 = Layer.provideMerge( + Layer.effect( + JobStorage, + Effect.gen(function* () { + const stack = yield* Stack; + const bucket = yield* S3.Bucket("JobsBucket"); + const queue = yield* SQS.Queue("JobsQueue").pipe( + RemovalPolicy.retain(stack.stage === "prod"), + ); + + const getObject = yield* S3.GetObject.bind(bucket); + const putObject = yield* S3.PutObject.bind(bucket); + const sink = yield* SQS.QueueSink.bind(queue); + + const putJob = (job: Job) => + putObject({ + Key: job.id, + Body: JSON.stringify(job), + }).pipe( + Effect.map(() => job), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new PutJobError({ + message: `Failed to store job "${job.id}": ${cause}`, + cause, + }), + ), + ), + ); + + const getJob = (jobId: string) => + getObject({ + Key: jobId, + }).pipe( + Effect.catchTag("NoSuchKey", () => Effect.succeed(undefined)), + Effect.flatMap( + (item) => + item?.Body?.pipe( + Stream.decodeText, + Stream.mkString, + Effect.flatMap((body) => + Effect.try({ + try: () => JSON.parse(body) as Job, + catch: (cause) => + new GetJobError({ + message: `Failed to parse job "${jobId}": ${cause}`, + cause, + }), + }), + ), + ) ?? Effect.succeed(undefined), + ), + Effect.tapError(Console.log), + Effect.catchCause((cause) => + Effect.fail( + new GetJobError({ + message: `Failed to load job "${jobId}": ${cause}`, + cause, + }), + ), + ), + ); + + yield* S3.notifications(bucket).subscribe((stream) => + stream.pipe( + Stream.flatMap((item) => + Stream.fromEffect(getJob(item.key).pipe(Effect.orDie)), + ), + Stream.filter((job): job is Job => job !== undefined), + Stream.map((job) => JSON.stringify(job)), + Stream.run(sink), + ), + ); + + return JobStorage.of({ + putJob, + getJob, + }); + }), + ), + Layer.mergeAll(Lambda.BucketEventSource, SQS.QueueSinkLive).pipe( + Layer.provideMerge( + Layer.mergeAll( + S3.GetObjectLive, + S3.PutObjectLive, + SQS.SendMessageBatchLive, + ), + ), + ), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda/test/integ.test.ts b/.repos/alchemy-effect/examples/aws-lambda/test/integ.test.ts new file mode 100644 index 00000000000..cba63151b21 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/test/integ.test.ts @@ -0,0 +1,23 @@ +import * as AWS from "alchemy/AWS"; +import * as Alchemy from "alchemy"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Effect from "effect/Effect"; +import Stack from "../alchemy.run.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: AWS.providers(), + state: Alchemy.localState(), +}); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "deploys and exposes a url", + Effect.gen(function* () { + const out = (yield* stack) as unknown; + const url = typeof out === "string" ? out : (out as { url: string }).url; + expect(url).toBeString(); + }), +); diff --git a/.repos/alchemy-effect/examples/aws-lambda/tsconfig.json b/.repos/alchemy-effect/examples/aws-lambda/tsconfig.json new file mode 100644 index 00000000000..0cf8c32afd6 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-lambda/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts", "test/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-rds/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-rds/alchemy.run.ts new file mode 100644 index 00000000000..ba39d17db6c --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rds/alchemy.run.ts @@ -0,0 +1,17 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Effect from "effect/Effect"; +import ServiceFunction from "./src/ServiceFunction.ts"; + +const aws = AWS.providers(); + +export default Alchemy.Stack( + "AwsRdsExample", + { providers: aws, state: Alchemy.localState() }, + Effect.gen(function* () { + const service = yield* ServiceFunction; + return { + url: service.functionUrl, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-rds/package.json b/.repos/alchemy-effect/examples/aws-rds/package.json new file mode 100644 index 00000000000..f34b01b25ad --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rds/package.json @@ -0,0 +1,25 @@ +{ + "name": "aws-rds-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-rds" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "@types/pg": "^8.18.0", + "alchemy": "workspace:*", + "effect": "catalog:", + "pg": "^8.20.0" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-rds/src/Database.ts b/.repos/alchemy-effect/examples/aws-rds/src/Database.ts new file mode 100644 index 00000000000..83587f73771 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rds/src/Database.ts @@ -0,0 +1,89 @@ +import * as AWS from "alchemy/AWS"; +import * as Context from "effect/Context"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { Client } from "pg"; +import { Network } from "./Network.ts"; + +export class SqlError extends Data.TaggedError("SqlError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class Database extends Context.Service< + Database, + { + query = Record>( + statement: string, + values?: ReadonlyArray, + ): Effect.Effect, SqlError>; + } +>()("Sql") {} + +export const DatabaseAurora = Layer.effect( + Database, + Effect.gen(function* () { + const { vpc, databaseSecurityGroup } = yield* Network; + const database = yield* AWS.RDS.Aurora("Database", { + subnetIds: vpc.privateSubnetIds, + securityGroupIds: [databaseSecurityGroup.groupId], + }); + + const connect = yield* AWS.RDS.Connect.bind(database.cluster, { + secret: database.secret, + subnetIds: vpc.privateSubnetIds, + securityGroupIds: [databaseSecurityGroup.groupId], + }); + + return Database.of({ + query: >( + statement: string, + values: ReadonlyArray = [], + ): Effect.Effect, SqlError> => + connect.pipe( + Effect.catch((cause) => + Effect.fail( + new SqlError({ + message: "Failed to resolve Aurora connection settings", + cause, + }), + ), + ), + Effect.flatMap((connection: AWS.RDS.ConnectionInfo) => + Effect.tryPromise({ + try: async () => { + const client = new Client({ + host: connection.host, + port: connection.port, + database: connection.database, + user: connection.username, + password: connection.password, + // Example-only SSL posture for private Aurora connections. + ssl: connection.ssl + ? { rejectUnauthorized: false } + : undefined, + }); + + await client.connect(); + try { + const result = await client.query({ + text: statement, + values: [...values], + }); + return result.rows as ReadonlyArray; + } finally { + await client.end(); + } + }, + catch: (cause) => + new SqlError({ + message: `Failed to execute SQL statement: ${statement}`, + cause, + }), + }), + ), + ), + }); + }), +); diff --git a/.repos/alchemy-effect/examples/aws-rds/src/Network.ts b/.repos/alchemy-effect/examples/aws-rds/src/Network.ts new file mode 100644 index 00000000000..227fdb5f395 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rds/src/Network.ts @@ -0,0 +1,58 @@ +import * as AWS from "alchemy/AWS"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +export interface ExampleNetwork { + vpc: AWS.EC2.Network; + functionSecurityGroup: AWS.EC2.SecurityGroup; + databaseSecurityGroup: AWS.EC2.SecurityGroup; + privateSecurityGroups: AWS.EC2.SecurityGroup[]; +} + +export class Network extends Context.Service()( + "Network", +) {} + +export const NetworkLive = Layer.effect( + Network, + Effect.gen(function* () { + const network = yield* AWS.EC2.Network("Network", { + cidrBlock: "10.0.0.0/16", + availabilityZones: 2, + nat: "single", + }); + + const functionSecurityGroup = yield* AWS.EC2.SecurityGroup( + "FunctionSecurityGroup", + { + vpcId: network.vpcId, + description: "Security group for the RDS example Lambda function", + }, + ); + + const databaseSecurityGroup = yield* AWS.EC2.SecurityGroup( + "DatabaseSecurityGroup", + { + vpcId: network.vpcId, + description: "Security group for the RDS example Aurora cluster", + ingress: [ + { + ipProtocol: "tcp", + fromPort: 5432, + toPort: 5432, + referencedGroupId: functionSecurityGroup.groupId, + description: "Allow Lambda to reach Aurora PostgreSQL", + }, + ], + }, + ); + + return { + vpc: network, + functionSecurityGroup, + databaseSecurityGroup, + privateSecurityGroups: [functionSecurityGroup], + } satisfies ExampleNetwork; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-rds/src/ServiceFunction.ts b/.repos/alchemy-effect/examples/aws-rds/src/ServiceFunction.ts new file mode 100644 index 00000000000..9d07ec4f616 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rds/src/ServiceFunction.ts @@ -0,0 +1,71 @@ +import * as AWS from "alchemy/AWS"; +import { Stack } from "alchemy/Stack"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Database, DatabaseAurora } from "./Database.ts"; +import { NetworkLive } from "./Network.ts"; + +export default class ServiceFunction extends AWS.Lambda.Function()( + "ServiceFunction", + Stack.useSync((stack) => ({ + main: import.meta.filename, + memory: stack.stage === "prod" ? 1024 : 512, + runtime: "nodejs24.x", + })), + Effect.gen(function* () { + const db = yield* Database; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + + if ( + request.method === "GET" && + new URL(request.originalUrl).pathname === "/" + ) { + const response = yield* db + .query<{ + database: string; + current_time: string; + current_user: string; + }>( + "select current_database() as database, now()::text as current_time, current_user::text as current_user", + ) + .pipe( + Effect.match({ + onFailure: (error) => ({ + status: 500 as const, + body: { + ok: false, + error: error.message, + }, + }), + onSuccess: (rows) => ({ + status: 200 as const, + body: { + ok: true, + connection: rows[0] ?? null, + }, + }), + }), + ); + + return yield* HttpServerResponse.json(response.body, { + status: response.status, + }); + } + + return HttpServerResponse.text("Not found", { status: 404 }); + }), + }; + }).pipe( + Effect.provide( + Layer.provideMerge( + Layer.mergeAll(DatabaseAurora), + Layer.mergeAll(NetworkLive, AWS.RDS.ConnectLive), + ), + ), + ), +) {} diff --git a/.repos/alchemy-effect/examples/aws-rds/tsconfig.json b/.repos/alchemy-effect/examples/aws-rds/tsconfig.json new file mode 100644 index 00000000000..e98040aede0 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rds/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "customConditions": ["bun"], + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-rest-api/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-rest-api/alchemy.run.ts new file mode 100644 index 00000000000..79400366052 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rest-api/alchemy.run.ts @@ -0,0 +1,84 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Output from "alchemy/Output"; +import * as Effect from "effect/Effect"; +import JobFunction from "./src/JobFunction.ts"; + +const aws = AWS.providers(); + +export default Alchemy.Stack( + "AwsRestApiProxy", + { + providers: aws, + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const { region, accountId } = yield* AWS.AWSEnvironment; + + const fn = yield* JobFunction; + + const api = yield* AWS.ApiGateway.RestApi("PublicApi", { + name: "alchemy-example-rest-api", + endpointConfiguration: { types: ["REGIONAL"] }, + }); + + const proxyResource = yield* AWS.ApiGateway.Resource("Proxy", { + restApiId: api.restApiId, + parentId: api.rootResourceId, + pathPart: "{proxy+}", + }); + + const invokeUri = Output.interpolate`arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${fn.functionArn}/invocations`; + + yield* AWS.ApiGateway.Method("RootAny", { + restApiId: api.restApiId, + resourceId: api.rootResourceId, + httpMethod: "ANY", + authorizationType: "NONE", + integration: { + type: "AWS_PROXY", + integrationHttpMethod: "POST", + uri: invokeUri, + }, + }); + + yield* AWS.ApiGateway.Method("ProxyAny", { + restApiId: api.restApiId, + resourceId: proxyResource.resourceId, + httpMethod: "ANY", + authorizationType: "NONE", + integration: { + type: "AWS_PROXY", + integrationHttpMethod: "POST", + uri: invokeUri, + }, + }); + + const deployment = yield* AWS.ApiGateway.Deployment("Release", { + restApiId: api.restApiId, + description: "initial", + triggers: { + rootMethod: "ANY", + proxyMethod: "ANY", + }, + }); + + const stage = yield* AWS.ApiGateway.Stage("ProdStage", { + restApiId: api.restApiId, + stageName: "prod", + deploymentId: deployment.deploymentId, + }); + + yield* AWS.Lambda.Permission("ApiGatewayInvoke", { + action: "lambda:InvokeFunction", + functionName: fn.functionName, + principal: "apigateway.amazonaws.com", + sourceArn: Output.interpolate`arn:aws:execute-api:${region}:${accountId}:${api.restApiId}/*/*/*`, + }); + + return { + invokeUrl: Output.interpolate`https://${api.restApiId}.execute-api.${region}.amazonaws.com/${stage.stageName}/`, + restApiId: api.restApiId, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-rest-api/package.json b/.repos/alchemy-effect/examples/aws-rest-api/package.json new file mode 100644 index 00000000000..afa92a0669e --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rest-api/package.json @@ -0,0 +1,22 @@ +{ + "name": "aws-rest-api", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-rest-api" + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} diff --git a/.repos/alchemy-effect/examples/aws-rest-api/src/JobFunction.ts b/.repos/alchemy-effect/examples/aws-rest-api/src/JobFunction.ts new file mode 100644 index 00000000000..7a48f093a64 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rest-api/src/JobFunction.ts @@ -0,0 +1,22 @@ +import * as AWS from "alchemy/AWS"; +import * as Effect from "effect/Effect"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +const main = import.meta.filename; + +/** + * Minimal Lambda for API Gateway `AWS_PROXY` — returns plain text. + */ +export default class JobFunction extends AWS.Lambda.Function()( + "JobFunction", + { + main, + }, + Effect.gen(function* () { + return { + fetch: Effect.gen(function* () { + return HttpServerResponse.text("Hello from REST API + Lambda"); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/examples/aws-rest-api/tsconfig.json b/.repos/alchemy-effect/examples/aws-rest-api/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-rest-api/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-static-site/README.md b/.repos/alchemy-effect/examples/aws-static-site/README.md new file mode 100644 index 00000000000..6575d03f8da --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-static-site/README.md @@ -0,0 +1,34 @@ +# AWS Static Site Example + +This example deploys a static website from the local `site/` directory using: + +- `AWS.Website.StaticSite` +- private S3 origin +- CloudFront distribution +- optional ACM certificate in `us-east-1` +- optional Route 53 alias records + +## Commands + +```sh +bun install +bun run --filter aws-static-site-example deploy +``` + +## Optional Custom Domain + +Set these environment variables before deploy: + +```sh +export WEBSITE_DOMAIN=www.example.com +export WEBSITE_ZONE_ID=Z1234567890 +export WEBSITE_ALIASES=example.com,static.example.com +``` + +When `WEBSITE_DOMAIN` and `WEBSITE_ZONE_ID` are provided, the example will: + +- request an ACM certificate +- create Route 53 validation records +- create Route 53 alias records to CloudFront + +Without them, the example still deploys and returns the CloudFront URL. diff --git a/.repos/alchemy-effect/examples/aws-static-site/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-static-site/alchemy.run.ts new file mode 100644 index 00000000000..b1c745b1a58 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-static-site/alchemy.run.ts @@ -0,0 +1,29 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Effect from "effect/Effect"; + +export default Alchemy.Stack( + "AwsStaticSiteExample", + { + providers: AWS.providers(), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const site = yield* AWS.Website.StaticSite("MarketingSite", { + path: "./site", + // domain: "your.domain.com", + forceDestroy: true, + invalidation: { + paths: "all", + }, + tags: { + Example: "aws-static-site", + Surface: "website", + }, + }); + + return { + url: site.url, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/aws-static-site/package.json b/.repos/alchemy-effect/examples/aws-static-site/package.json new file mode 100644 index 00000000000..355f7815215 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-static-site/package.json @@ -0,0 +1,24 @@ +{ + "name": "aws-static-site-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-static-site" + }, + "type": "module", + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-static-site/site/docs/index.html b/.repos/alchemy-effect/examples/aws-static-site/site/docs/index.html new file mode 100644 index 00000000000..5f93bbe1250 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-static-site/site/docs/index.html @@ -0,0 +1,22 @@ + + + + + + Static Site Docs + + + +
+

docs route

+

Static hosting can ship multiple pages too.

+

+ This page is just another file in the deployed folder and is cached by + CloudFront as a normal static asset. +

+
+ Back home +
+
+ + diff --git a/.repos/alchemy-effect/examples/aws-static-site/site/index.html b/.repos/alchemy-effect/examples/aws-static-site/site/index.html new file mode 100644 index 00000000000..ecbcda0690a --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-static-site/site/index.html @@ -0,0 +1,25 @@ + + + + + + Alchemy AWS Static Site + + + +
+

alchemy example

+

AWS static site with CloudFront, ACM, and Route 53

+

+ This site is deployed from a plain folder using + AWS.Website.StaticSite. +

+ +
+ + diff --git a/.repos/alchemy-effect/examples/aws-static-site/site/styles.css b/.repos/alchemy-effect/examples/aws-static-site/site/styles.css new file mode 100644 index 00000000000..933f3357492 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-static-site/site/styles.css @@ -0,0 +1,56 @@ +html, +body { + margin: 0; + min-height: 100%; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + background: linear-gradient(180deg, #0f172a 0%, #111827 100%); + color: #e5e7eb; +} + +.page { + max-width: 48rem; + margin: 0 auto; + padding: 6rem 1.5rem; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.14em; + color: #93c5fd; + font-size: 0.875rem; +} + +h1 { + font-size: clamp(2.5rem, 6vw, 4.5rem); + line-height: 1.05; + margin: 0.5rem 0 1rem; +} + +.lede { + font-size: 1.125rem; + line-height: 1.7; + color: #cbd5e1; +} + +.actions { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-top: 2rem; +} + +.actions a { + color: #0f172a; + background: #f8fafc; + text-decoration: none; + padding: 0.85rem 1.1rem; + border-radius: 999px; + font-weight: 600; +} diff --git a/.repos/alchemy-effect/examples/aws-static-site/tsconfig.json b/.repos/alchemy-effect/examples/aws-static-site/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-static-site/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-vite/README.md b/.repos/alchemy-effect/examples/aws-vite/README.md new file mode 100644 index 00000000000..08ba6c988cb --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/README.md @@ -0,0 +1,33 @@ +# AWS Vite Example + +This example builds a Vite app and deploys it with: + +- `Build.Build` for the frontend build step +- `AWS.Website.StaticSite` for S3 + CloudFront hosting +- optional ACM + Route 53 custom-domain wiring + +## Commands + +```sh +bun install +bun run --filter aws-vite-example build +bun run --filter aws-vite-example deploy +``` + +For local frontend development: + +```sh +bun run --filter aws-vite-example dev:vite +``` + +## Optional Custom Domain + +Set these environment variables before deploy: + +```sh +export WEBSITE_DOMAIN=app.example.com +export WEBSITE_ZONE_ID=Z1234567890 +export WEBSITE_ALIASES=www.app.example.com +``` + +Without them, the example still deploys and returns the CloudFront URL. diff --git a/.repos/alchemy-effect/examples/aws-vite/alchemy.run.ts b/.repos/alchemy-effect/examples/aws-vite/alchemy.run.ts new file mode 100644 index 00000000000..c863000acb9 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/alchemy.run.ts @@ -0,0 +1,103 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Output from "alchemy/Output"; +import * as Config from "effect/Config"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; + +const aws = AWS.providers(); + +const WEBSITE_DOMAIN = Config.string("WEBSITE_DOMAIN").pipe( + Config.option, + Config.map(Option.getOrUndefined), + (config) => config.asEffect(), +); + +const WEBSITE_ZONE_ID = Config.string("WEBSITE_ZONE_ID").pipe( + Config.option, + Config.map(Option.getOrUndefined), + (config) => config.asEffect(), +); + +const WEBSITE_ALIASES = Config.string("WEBSITE_ALIASES").pipe( + Config.option, + Config.map(Option.getOrUndefined), + Config.map((value) => + value + ?.split(",") + .map((part) => part.trim()) + .filter(Boolean), + ), + (config) => config.asEffect(), +); + +export default Alchemy.Stack( + "AwsViteExample", + { providers: aws, state: Alchemy.localState() }, + Effect.gen(function* () { + /** + * Optional Route 53 / ACM config. + * + * Set these before deploying if you want a custom domain: + * - WEBSITE_DOMAIN=app.example.com + * - WEBSITE_ZONE_ID=Z1234567890 + * - WEBSITE_ALIASES=www.app.example.com + */ + const websiteDomainName = yield* WEBSITE_DOMAIN; + const websiteZoneId = yield* WEBSITE_ZONE_ID; + const websiteAliases = yield* WEBSITE_ALIASES; + const websiteDomain = + websiteDomainName && websiteZoneId + ? { + name: websiteDomainName, + hostedZoneId: websiteZoneId, + aliases: websiteAliases, + } + : undefined; + + const site = yield* AWS.Website.StaticSite("FrontendSite", { + path: ".", + build: { + command: "bun run build", + output: "dist", + }, + environment: { + VITE_STAGE: "test", + }, + spa: true, + cdn: false, + tags: { + Example: "aws-vite", + Surface: "website", + }, + }); + + const router = yield* AWS.Website.Router("FrontendRouter", { + domain: websiteDomain, + routes: { + "/*": site.routeTarget, + }, + invalidation: { + paths: "all", + }, + tags: { + Example: "aws-vite", + Surface: "website", + Mode: "router", + }, + }); + + return { + url: router.url, + cloudFrontDomain: router.distribution.domainName, + distributionId: router.distribution.distributionId, + bucketName: site.bucket.bucketName, + buildHash: site.build?.hash, + assetVersion: site.files.version, + certificateArn: router.certificate?.certificateArn as any, + customDomain: websiteDomain?.name, + aliasRecordNames: router.records.map((record) => record.name), + previewHint: Output.interpolate`Run bun run dev:vite for local frontend iteration, then deploy to publish ${router.distribution.domainName}`, + }; + }) as any, +); diff --git a/.repos/alchemy-effect/examples/aws-vite/index.html b/.repos/alchemy-effect/examples/aws-vite/index.html new file mode 100644 index 00000000000..8d58e614109 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/index.html @@ -0,0 +1,12 @@ + + + + + + AWS Vite Example + + + +
+ + diff --git a/.repos/alchemy-effect/examples/aws-vite/package.json b/.repos/alchemy-effect/examples/aws-vite/package.json new file mode 100644 index 00000000000..372bde03e52 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/package.json @@ -0,0 +1,31 @@ +{ + "name": "aws-vite-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/aws-vite" + }, + "type": "module", + "scripts": { + "build": "vite build", + "dev:vite": "vite", + "preview": "vite preview", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@cloudflare/vite-plugin": "catalog:", + "@distilled.cloud/aws": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "vite": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/aws-vite/src/main.ts b/.repos/alchemy-effect/examples/aws-vite/src/main.ts new file mode 100644 index 00000000000..9ddf3e2dc36 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/src/main.ts @@ -0,0 +1,24 @@ +import "./style.css"; + +const app = document.querySelector("#app"); + +if (!app) { + throw new Error("Missing #app root element"); +} + +app.innerHTML = ` +
+

alchemy + vite

+

Deploy a Vite SPA to AWS with CloudFront, ACM, and Route 53.

+

+ This example runs a Vite build and publishes the generated assets with + AWS.Website.StaticSite behind a shared + AWS.Website.Router. +

+
    +
  • Build artifacts uploaded to a private S3 bucket
  • +
  • CloudFront Router in front of the site
  • +
  • Optional custom domain with ACM validation and Route 53 alias records
  • +
+
+`; diff --git a/.repos/alchemy-effect/examples/aws-vite/src/style.css b/.repos/alchemy-effect/examples/aws-vite/src/style.css new file mode 100644 index 00000000000..edc2347439c --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/src/style.css @@ -0,0 +1,51 @@ +html, +body { + margin: 0; + min-height: 100%; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + background: #020617; + color: #e2e8f0; +} + +#app { + min-height: 100vh; +} + +.page { + max-width: 56rem; + margin: 0 auto; + padding: 6rem 1.5rem; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.16em; + color: #60a5fa; + font-size: 0.875rem; +} + +h1 { + margin: 0.75rem 0 1rem; + font-size: clamp(2.5rem, 6vw, 4.25rem); + line-height: 1.05; +} + +.lede { + font-size: 1.125rem; + line-height: 1.7; + color: #cbd5e1; +} + +.highlights { + margin: 2rem 0 0; + padding-left: 1.25rem; + color: #bfdbfe; + line-height: 1.9; +} diff --git a/.repos/alchemy-effect/examples/aws-vite/tsconfig.json b/.repos/alchemy-effect/examples/aws-vite/tsconfig.json new file mode 100644 index 00000000000..9e1b6430561 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts", "vite.config.ts"], + "compilerOptions": { + "noEmit": true, + // TODO(sam): remove this once we have a proper implementation + "noCheck": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/aws-vite/vite.config.ts b/.repos/alchemy-effect/examples/aws-vite/vite.config.ts new file mode 100644 index 00000000000..dbd8cc94be4 --- /dev/null +++ b/.repos/alchemy-effect/examples/aws-vite/vite.config.ts @@ -0,0 +1,6 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [cloudflare()], +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/.gitignore b/.repos/alchemy-effect/examples/cloudflare-dev/.gitignore new file mode 100644 index 00000000000..532db46c01b --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/.gitignore @@ -0,0 +1 @@ +!src/modules/modules.d.ts \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-dev/alchemy.run.ts new file mode 100644 index 00000000000..b070f57499f --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/alchemy.run.ts @@ -0,0 +1,44 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Config from "effect/Config"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import type { Counter as CounterClass } from "./src/AsyncWorker.ts"; +import EffectWorker from "./src/EffectWorker.ts"; + +export const Counter = Cloudflare.DurableObjectNamespace( + "Counter", + { + className: "Counter", + }, +); + +export type AsyncWorkerEnv = Cloudflare.InferEnv; + +export const AsyncWorker = Cloudflare.Worker("AsyncWorker", { + main: "./src/AsyncWorker.ts", + env: { + COUNTER: Counter, + MY_VARIABLE: "my-variable-abc123", + MY_SECRET: Config.redacted("MY_SECRET").pipe( + Config.withDefault(Redacted.make("my-secret-abc123")), + ), + }, +}); + +export default Alchemy.Stack( + "CloudflareDev", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const asyncWorker = yield* AsyncWorker; + const effectWorker = yield* EffectWorker; + + return { + asyncWorker: asyncWorker.url, + effectWorker: effectWorker.url, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/package.json b/.repos/alchemy-effect/examples/cloudflare-dev/package.json new file mode 100644 index 00000000000..7d226cf5dd4 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/package.json @@ -0,0 +1,27 @@ +{ + "name": "cloudflare-dev", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-dev" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + }, + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/src/AsyncWorker.ts b/.repos/alchemy-effect/examples/cloudflare-dev/src/AsyncWorker.ts new file mode 100644 index 00000000000..0a8d51a30e7 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/src/AsyncWorker.ts @@ -0,0 +1,40 @@ +import { DurableObject } from "cloudflare:workers"; +import type { AsyncWorkerEnv } from "../alchemy.run.ts"; +import wasm from "./modules/wasm-example.wasm"; + +interface AddInstance { + exports: { + add(a: number, b: number): number; + }; +} + +export default { + async fetch(request, env) { + const url = new URL(request.url); + switch (url.pathname) { + case "/env": + return Response.json(env); + case "/wasm": + const instance = (await WebAssembly.instantiate(wasm)) as AddInstance; + return Response.json({ result: instance.exports.add(3, 4) }); + default: + const counter = env.COUNTER.getByName("my-counter"); + const count = await counter.increment(); + return new Response(`Hello, world! ${count}`); + } + }, +} satisfies ExportedHandler; + +export class Counter extends DurableObject { + async increment() { + return ++this.counter; + } + + get counter() { + return this.ctx.storage.kv.get("counter") ?? 0; + } + + set counter(value: number) { + this.ctx.storage.kv.put("counter", value); + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/src/EffectWorker.ts b/.repos/alchemy-effect/examples/cloudflare-dev/src/EffectWorker.ts new file mode 100644 index 00000000000..541e40b6aca --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/src/EffectWorker.ts @@ -0,0 +1,65 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { KV } from "./KV.ts"; +import NotifyWorkflow from "./NotifyWorkflow.ts"; + +interface AddInstance { + exports: { + add(a: number, b: number): number; + }; +} + +export default class EffectWorker extends Cloudflare.Worker()( + "EffectWorker", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const kv = yield* Cloudflare.KVNamespace.bind(KV); + const workflow = yield* NotifyWorkflow; + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + const url = new URL(request.url, "http://internal"); + if (url.pathname === "/wasm") { + const instance = yield* Effect.promise(async () => { + // This is dynamically imported so that the WASM import doesn't occur at deploy-time, which works in Bun but fails in Node. + const wasm = await import("./modules/wasm-example.wasm"); + return (await WebAssembly.instantiate(wasm.default)) as AddInstance; + }); + return yield* HttpServerResponse.json({ + result: instance.exports.add(3, 4), + }); + } else if (url.pathname.startsWith("/workflow/start/")) { + const roomId = url.pathname.split("/workflow/start/")[1]; + if (!roomId) { + return yield* HttpServerResponse.json( + { error: "roomId is required" }, + { status: 400 }, + ); + } + const instance = yield* workflow.create({ + roomId, + message: "hello from workflow", + }); + return yield* HttpServerResponse.json({ instanceId: instance.id }); + } else if (url.pathname.startsWith("/workflow/status/")) { + const instanceId = url.pathname.split("/workflow/status/")[1]; + if (!instanceId) { + return yield* HttpServerResponse.json( + { error: "instanceId is required" }, + { status: 400 }, + ); + } + const instance = yield* workflow.get(instanceId); + const status = yield* instance.status(); + return yield* HttpServerResponse.json(status); + } + const value = yield* kv.list().pipe(Effect.orDie); + return yield* HttpServerResponse.json(value); + }), + }; + }).pipe(Effect.provide([Cloudflare.KVNamespaceBindingLive])), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/src/KV.ts b/.repos/alchemy-effect/examples/cloudflare-dev/src/KV.ts new file mode 100644 index 00000000000..a423af5f86b --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/src/KV.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const KV = Cloudflare.KVNamespace("KV"); diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/src/NotifyWorkflow.ts b/.repos/alchemy-effect/examples/cloudflare-dev/src/NotifyWorkflow.ts new file mode 100644 index 00000000000..be3701d86ff --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/src/NotifyWorkflow.ts @@ -0,0 +1,83 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Config from "effect/Config"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import { KV } from "./KV.ts"; + +/** + * Hard-coded value the integ test asserts on to prove the secret was + * bound at plantime and read at runtime through the `Redacted` accessor. + */ +export const WORKFLOW_SECRET_VALUE = "wf-secret-abc123"; + +export default class NotifyWorkflow extends Cloudflare.Workflow()( + "Notifier", + Effect.gen(function* () { + // Bind a `secret_text` on the workflow at plantime. Using a literal + // (instead of `Config.redacted("WORKFLOW_SECRET")`) keeps the integ + // test self-contained — no `.env` setup required. + const secret = yield* Config.redacted("WORKFLOW_SECRET").pipe( + Config.withDefault(Redacted.make(WORKFLOW_SECRET_VALUE)), + ); + // Regression guard for https://github.com/alchemy-run/alchemy-effect/pull/71 + // + // The kv binding internally yields `Cloudflare.WorkerEnvironment` — + // before that PR, accessing `WorkerEnvironment` inside a workflow body + // crashed because `provideService(WorkerEnvironment, env)` was applied + // to the outer `Effect.succeed(body)` wrapper (a no-op) instead of + // `body` itself in `Workflow.ts`. Exercising `kv.put` / `kv.get` from + // inside a `task` keeps the integ test catching any future regression. + const kv = yield* Cloudflare.KVNamespace.bind(KV); + + return Effect.fn(function* (input: { roomId: string; message: string }) { + const { roomId, message } = input; + + const stored = yield* Cloudflare.task( + "kv-roundtrip", + Effect.gen(function* () { + const key = `workflow:smoke:${roomId}`; + yield* kv.put(key, message); + const got = yield* kv.get(key); + if (got !== message) { + return yield* Effect.die( + new Error( + `KV roundtrip mismatch: expected "${message}", got "${got ?? "null"}"`, + ), + ); + } + return got; + }).pipe(Effect.orDie), + ); + + // Resolve the bound secret inside the workflow body. The accessor + // returns `Redacted`; unwrap only where the value needs to + // leave the workflow (here, in the broadcast + the returned output + // so the integ test can assert end-to-end propagation). + const secretValue = Redacted.value(secret); + + const processed = yield* Cloudflare.task( + "process", + Effect.succeed({ + text: `Processed: ${stored}`, + secret: secretValue, + ts: Date.now(), + }), + ); + + // const room = rooms.getByName(roomId); + // yield* Cloudflare.task( + // "broadcast", + // room.broadcast(`[workflow] ${processed.text} secret=${secretValue}`), + // ); + + // yield* Cloudflare.sleep("cooldown", "2 seconds"); + + // yield* Cloudflare.task( + // "finalize", + // room.broadcast(`[workflow] complete for ${roomId}`), + // ); + + return processed; + }); + }), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/src/modules/modules.d.ts b/.repos/alchemy-effect/examples/cloudflare-dev/src/modules/modules.d.ts new file mode 100644 index 00000000000..d3e824fde93 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/src/modules/modules.d.ts @@ -0,0 +1,4 @@ +declare module "*.wasm" { + const content: Uint8Array; + export default content; +} diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/src/modules/wasm-example.wasm b/.repos/alchemy-effect/examples/cloudflare-dev/src/modules/wasm-example.wasm new file mode 100644 index 00000000000..357f72da7a0 Binary files /dev/null and b/.repos/alchemy-effect/examples/cloudflare-dev/src/modules/wasm-example.wasm differ diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-dev/test/integ.test.ts new file mode 100644 index 00000000000..1e17faa6808 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/test/integ.test.ts @@ -0,0 +1,190 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import Stack from "../alchemy.run.ts"; +import { WORKFLOW_SECRET_VALUE } from "../src/NotifyWorkflow.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), + dev: true, +}); + +const stack = beforeAll(deploy(Stack)); + +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "deploys all workers with URLs", + Effect.gen(function* () { + const { asyncWorker, effectWorker } = yield* stack; + + expect(asyncWorker).toBeString(); + expect(effectWorker).toBeString(); + }), +); + +/** + * AsyncWorker exports a default fetch handler that calls the `Counter` + * Durable Object's `increment()` and returns `Hello, world! `. + * + * Hitting the worker twice exercises the DO end-to-end and proves + * persistent state across requests — if the DO binding is missing or + * the class export is wrong, the first request fails outright. + */ +test( + "AsyncWorker increments the Counter Durable Object across requests", + Effect.gen(function* () { + const { asyncWorker } = yield* stack; + const url = asyncWorker!; + + const first = yield* HttpClient.get(url); + expect(first.status).toBe(200); + const firstBody = yield* first.text; + const firstMatch = firstBody.match(/^Hello, world! (\d+)$/); + expect(firstMatch).not.toBeNull(); + const firstCount = Number(firstMatch![1]); + + const second = yield* HttpClient.get(url); + expect(second.status).toBe(200); + const secondBody = yield* second.text; + const secondMatch = secondBody.match(/^Hello, world! (\d+)$/); + expect(secondMatch).not.toBeNull(); + const secondCount = Number(secondMatch![1]); + + expect(secondCount).toBe(firstCount + 1); + }), +); + +test( + "AsyncWorker receives bindings, including variables and secrets", + Effect.gen(function* () { + const { asyncWorker } = yield* stack; + const response = yield* HttpClient.get(new URL("/env", asyncWorker!)); + expect(response.status).toBe(200); + const body = yield* response.json; + expect(body).toMatchObject({ + MY_SECRET: "my-secret-abc123", + MY_VARIABLE: "my-variable-abc123", + COUNTER: {}, + }); + }), +); + +/** + * EffectWorker binds a KV namespace via `Cloudflare.KVNamespace.bind(KV)` + * and returns the result of `kv.list()` as JSON. A successful response + * proves the Effect-style binding wired the runtime SDK and the + * `WorkerEnvironment` service was provisioned for the fetch handler. + */ +test( + "EffectWorker returns a KV list result via the Effect KV binding", + Effect.gen(function* () { + const { effectWorker } = yield* stack; + + const response = yield* HttpClient.get(effectWorker!); + expect(response.status).toBe(200); + + const body = (yield* response.json) as { + keys: Array<{ name: string }>; + list_complete: boolean; + }; + expect(Array.isArray(body.keys)).toBe(true); + expect(typeof body.list_complete).toBe("boolean"); + }), +); + +/** + * Both workers import `./modules/wasm-example.wasm`, which exports a + * single `add(a: number, b: number): number` function. Hitting `/wasm` + * instantiates the module and returns `add(3, 4)` as JSON, proving that + * the bundler ships the wasm asset to workerd and that runtime + * `WebAssembly.instantiate` works for both the raw async-handler and + * Effect-style entrypoints. + */ +test( + "AsyncWorker /wasm instantiates the wasm module and returns add(3, 4)", + Effect.gen(function* () { + const { asyncWorker } = yield* stack; + + const response = yield* HttpClient.get(new URL("/wasm", asyncWorker!)); + expect(response.status).toBe(200); + const body = (yield* response.json) as { result: number }; + expect(body.result).toBe(7); + }), +); + +test( + "EffectWorker /wasm instantiates the wasm module and returns add(3, 4)", + Effect.gen(function* () { + const { effectWorker } = yield* stack; + + const response = yield* HttpClient.get(new URL("/wasm", effectWorker!)); + expect(response.status).toBe(200); + const body = (yield* response.json) as { result: number }; + expect(body.result).toBe(7); + }), +); + +interface WorkflowStatus { + status: string; + output?: { text?: string; secret?: string; ts?: number }; + error?: { name: string; message: string } | null; +} + +/** + * Start a `NotifyWorkflow` instance through `workerUrl` and poll its status + * until the instance reaches a terminal state. Asserts the workflow ran the + * KV roundtrip task (`Processed: `) and resolved the plantime-bound + * `Alchemy.Secret` at runtime (`output.secret === WORKFLOW_SECRET_VALUE`). + */ +const exerciseWorkflow = (workerUrl: string, label: string) => + Effect.gen(function* () { + const roomId = `${label}-${Math.random().toString(36).slice(2, 10)}`; + + const startResponse = yield* HttpClient.post( + new URL(`/workflow/start/${roomId}`, workerUrl), + ); + expect(startResponse.status).toBe(200); + const { instanceId } = (yield* startResponse.json) as { + instanceId: string; + }; + expect(instanceId).toBeString(); + + const statusUrl = new URL(`/workflow/status/${instanceId}`, workerUrl); + const fetchStatus = HttpClient.get(statusUrl).pipe( + Effect.flatMap((res) => res.json), + Effect.map((json) => json as unknown as WorkflowStatus), + ); + const status = yield* fetchStatus.pipe( + Effect.repeat({ + schedule: Schedule.spaced("2 seconds"), + until: (s: WorkflowStatus) => + s.status === "complete" || s.status === "errored", + times: 60, + }), + ); + + expect(status.error).toBeFalsy(); + expect(status.status).toBe("complete"); + expect(status.output?.secret).toBe(WORKFLOW_SECRET_VALUE); + expect(status.output?.text).toBe("Processed: hello from workflow"); + }); + +/** + * EffectWorker `/workflow/start/:roomId` kicks off `NotifyWorkflow`, which + * does a KV roundtrip task and resolves the `WORKFLOW_SECRET` `Alchemy.Secret` + * at runtime. The status route surfaces the workflow output so we can assert + * the workflow actually executed end-to-end (not just that it was scheduled). + */ +test( + "EffectWorker drives NotifyWorkflow to completion with secret + KV roundtrip", + Effect.gen(function* () { + const { effectWorker } = yield* stack; + yield* exerciseWorkflow(effectWorker!, "effect"); + }), + { timeout: 180_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-dev/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-dev/tsconfig.json new file mode 100644 index 00000000000..11462becb0a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-dev/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts", "src/**/*.d.ts", "test/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "types": ["bun", "@cloudflare/workers-types"] + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-email/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-email/alchemy.run.ts new file mode 100644 index 00000000000..9ae01e6030e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-email/alchemy.run.ts @@ -0,0 +1,29 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +import Api from "./src/Api.ts"; +import { Destination, InboxRule, Routing } from "./src/Email.ts"; + +export default Alchemy.Stack( + "CloudflareEmailExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const routing = yield* Routing; + const destination = yield* Destination; + const rule = yield* InboxRule; + const api = yield* Api; + + return { + url: api.url.as(), + zoneId: routing.zoneId, + routingEnabled: routing.enabled, + destinationEmail: destination.email, + destinationVerified: destination.verified, + ruleId: rule.ruleId, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-email/package.json b/.repos/alchemy-effect/examples/cloudflare-email/package.json new file mode 100644 index 00000000000..15338650cf5 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-email/package.json @@ -0,0 +1,24 @@ +{ + "name": "cloudflare-email", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alchemy-run/alchemy-effect" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "test": "bun test" + }, + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-email/src/Api.ts b/.repos/alchemy-effect/examples/cloudflare-email/src/Api.ts new file mode 100644 index 00000000000..b4bd06b9e8e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-email/src/Api.ts @@ -0,0 +1,67 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { DESTINATION, SendEmail, SENDER } from "./Email.ts"; + +/** + * Minimal email service Worker. + * + * - `GET /healthz` — liveness probe. + * - `POST /send` body `{ subject, text }` — sends mail from the bound + * sender to the bound destination via the Worker's `send_email` + * binding. Returns `{ ok: true }` on success or `{ ok: false, message }` + * on a Cloudflare-side rejection (e.g. unverified destination). + */ +export default class Api extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + subdomain: { enabled: true, previewsEnabled: false }, + compatibility: { date: "2024-09-23", flags: ["nodejs_compat"] }, + }, + Effect.gen(function* () { + const email = yield* Cloudflare.SendEmail.bind(SendEmail); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.url, "http://x"); + + if (url.pathname === "/healthz") { + return yield* HttpServerResponse.json({ + ok: true, + from: SENDER, + to: DESTINATION, + }); + } + + if (url.pathname === "/send" && request.method === "POST") { + const body = (yield* request.json) as { + subject?: string; + text?: string; + }; + const result = yield* email + .send({ + from: SENDER, + to: DESTINATION, + subject: body.subject ?? "alchemy email example", + text: body.text ?? `sent at ${new Date().toISOString()}`, + }) + .pipe( + Effect.match({ + onSuccess: () => ({ ok: true as const }), + onFailure: (err) => ({ + ok: false as const, + message: err.message, + }), + }), + ); + return yield* HttpServerResponse.json(result); + } + + return HttpServerResponse.text("not found", { status: 404 }); + }), + }; + }).pipe(Effect.provide(Cloudflare.SendEmailBindingLive)), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-email/src/Email.ts b/.repos/alchemy-effect/examples/cloudflare-email/src/Email.ts new file mode 100644 index 00000000000..0a69f2e6343 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-email/src/Email.ts @@ -0,0 +1,60 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +/** + * Cloudflare zone the example operates on. The same zone is used for + * inbound (Email Routing) and outbound (`send_email` sender domain). + */ +export const ZONE = "alchemy-test-2.us"; + +/** + * `from:` address the Worker is allowed to send mail as. Must live on a + * sender domain with Email Routing enabled (the `Routing` resource below + * takes care of that). + */ +export const SENDER = process.env.CLOUDFLARE_EMAIL_FROM ?? `bot@${ZONE}`; + +/** + * Destination the rules forward inbound mail to and that the Worker is + * pinned to send to. Cloudflare requires this address to be verified + * (recipient clicks a confirmation link) before any rule will deliver. + */ +export const DESTINATION = process.env.CLOUDFLARE_EMAIL_TO ?? "sam@alchemy.run"; + +/** + * Enable Email Routing on the zone. This is the prerequisite for both + * receiving mail (rules) and sending mail from a Worker (`send_email` + * sender domain). + */ +export const Routing = Cloudflare.EmailRouting("Routing", { + zone: ZONE, +}); + +/** + * Register the destination address on the account. Cloudflare emails a + * verification link the first time this address is added; until the + * recipient clicks it, rules forwarding here will silently drop. + */ +export const Destination = Cloudflare.EmailAddress("Destination", { + email: DESTINATION, +}); + +/** + * Forward inbound `inbox@` mail to the verified destination. The + * matcher is a literal `to:` match; everything else falls through to + * whatever catch-all rule (if any) is configured on the zone. + */ +export const InboxRule = Cloudflare.EmailRule("InboxRule", { + zone: ZONE, + name: "Forward inbox to destination", + matchers: [{ type: "literal", field: "to", value: `inbox@${ZONE}` }], + actions: [{ type: "forward", value: [DESTINATION] }], +}); + +/** + * `send_email` Worker binding restricted to the sender/destination pair + * above so the Worker can't be tricked into emailing arbitrary recipients. + */ +export const SendEmail = Cloudflare.SendEmail("Email", { + allowedSenderAddresses: [SENDER], + destinationAddress: DESTINATION, +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-email/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-email/test/integ.test.ts new file mode 100644 index 00000000000..bbdbbecc172 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-email/test/integ.test.ts @@ -0,0 +1,95 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import * as HttpBody from "effect/unstable/http/HttpBody"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "../alchemy.run.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), +}); + +const stack = beforeAll(deploy(Stack)); + +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +// workers.dev URLs take a few seconds to propagate after first enable. +const getOnce = (url: string) => + Effect.gen(function* () { + const response = yield* HttpClient.get(url); + if (response.status === 404) { + return yield* Effect.fail(new Error("workers.dev not yet propagated")); + } + return response; + }).pipe(Effect.retry({ schedule: Schedule.spaced("1 second"), times: 30 })); + +test( + "stack outputs reflect the deployed email infrastructure", + Effect.gen(function* () { + const out = yield* stack; + expect(out.url).toBeString(); + expect(out.zoneId).toBeString(); + expect(out.routingEnabled).toBe(true); + expect(out.destinationEmail).toBeString(); + expect(out.ruleId).toBeString(); + }), +); + +test( + "worker exposes the configured sender/destination on /healthz", + Effect.gen(function* () { + const { url } = yield* stack; + const response = yield* getOnce(`${url.replace(/\/+$/, "")}/healthz`); + expect(response.status).toBe(200); + const body = (yield* response.json) as { + ok: boolean; + from: string; + to: string; + }; + expect(body.ok).toBe(true); + expect(body.from).toBeString(); + expect(body.to).toBeString(); + }), + { timeout: 60_000 }, +); + +test( + "worker sends an email via the send_email binding", + Effect.gen(function* () { + const { url } = yield* stack; + const baseUrl = url.replace(/\/+$/, ""); + + yield* getOnce(baseUrl); + + const response = yield* HttpClient.execute( + HttpClientRequest.post(`${baseUrl}/send`).pipe( + HttpClientRequest.setBody( + HttpBody.text( + JSON.stringify({ + subject: `alchemy integ ${Date.now()}`, + text: "hello from cloudflare-email integ.test.ts", + }), + "application/json", + ), + ), + ), + ); + expect(response.status).toBe(200); + const body = (yield* response.json) as { + ok: boolean; + message?: string; + }; + if (!body.ok) { + // Surface the Cloudflare-side error so the failure is debuggable. + // Most often: "destination address not verified" until a human + // clicks the link sent by EmailAddress. + throw new Error(`send_email rejected the message: ${body.message}`); + } + expect(body.ok).toBe(true); + }), + { timeout: 120_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-email/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-email/tsconfig.json new file mode 100644 index 00000000000..0cf8c32afd6 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-email/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts", "test/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/alchemy.run.ts new file mode 100644 index 00000000000..14eb7a4c9ef --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/alchemy.run.ts @@ -0,0 +1,20 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +import Api from "./src/Worker.ts"; + +export default Alchemy.Stack( + "CloudflareGitArtifacts", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const api = yield* Api; + + return { + url: api.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/package.json b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/package.json new file mode 100644 index 00000000000..a11fc3bcb86 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/package.json @@ -0,0 +1,26 @@ +{ + "name": "cloudflare-git-artifacts", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-git-artifacts" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + }, + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Api.ts b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Api.ts new file mode 100644 index 00000000000..cafde83861e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Api.ts @@ -0,0 +1,129 @@ +import * as Schema from "effect/Schema"; +import * as HttpApi from "effect/unstable/httpapi/HttpApi"; +import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint"; +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup"; +import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema"; + +// ─── Domain types ──────────────────────────────────────────────────── + +export class Metadata extends Schema.Class("Metadata")({ + description: Schema.String, + topics: Schema.Array(Schema.String), + stars: Schema.Number, + createdAt: Schema.Number, +}) {} + +export class RepoInfo extends Schema.Class("RepoInfo")({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + defaultBranch: Schema.String, + remote: Schema.String, + status: Schema.String, + readOnly: Schema.Boolean, + createdAt: Schema.String, + updatedAt: Schema.String, + lastPushAt: Schema.NullOr(Schema.String), + metadata: Schema.NullOr(Metadata), +}) {} + +export class CreateRepoResponse extends Schema.Class( + "CreateRepoResponse", +)({ + name: Schema.String, + remote: Schema.String, + token: Schema.String, + defaultBranch: Schema.String, +}) {} + +export class CloneToken extends Schema.Class("CloneToken")({ + id: Schema.String, + plaintext: Schema.String, + scope: Schema.Literals(["read", "write"]), + expiresAt: Schema.String, +}) {} + +// ─── Errors ───────────────────────────────────────────────────────── + +export class RepoNotFound extends Schema.TaggedErrorClass()( + "RepoNotFound", + { name: Schema.String }, +) {} + +export class RepoConflict extends Schema.TaggedErrorClass()( + "RepoConflict", + { message: Schema.String }, +) {} + +// ─── Path / payload schemas ────────────────────────────────────────── + +const RepoNameParam = Schema.Struct({ name: Schema.String }); + +const CreateRepoPayload = Schema.Struct({ + name: Schema.String, + description: Schema.optional(Schema.String), +}); + +const UpdateRepoPayload = Schema.Struct({ + description: Schema.optional(Schema.String), + topics: Schema.optional(Schema.Array(Schema.String)), +}); + +const CloneTokenPayload = Schema.Struct({ + scope: Schema.optional(Schema.Literals(["read", "write"])), + ttl: Schema.optional(Schema.Number), +}); + +// ─── Endpoints ────────────────────────────────────────────────────── + +export const createRepo = HttpApiEndpoint.post("createRepo", "/repos", { + payload: CreateRepoPayload, + success: CreateRepoResponse, + error: RepoConflict, +}); + +export const getRepo = HttpApiEndpoint.get("getRepo", "/repos/:name", { + params: RepoNameParam, + success: RepoInfo, + error: RepoNotFound, +}); + +export const updateRepo = HttpApiEndpoint.patch("updateRepo", "/repos/:name", { + params: RepoNameParam, + payload: UpdateRepoPayload, + success: Metadata, + error: RepoNotFound, +}); + +export const deleteRepo = HttpApiEndpoint.delete("deleteRepo", "/repos/:name", { + params: RepoNameParam, + success: HttpApiSchema.NoContent, + error: RepoNotFound, +}); + +export const starRepo = HttpApiEndpoint.post("starRepo", "/repos/:name/star", { + params: RepoNameParam, + success: Metadata, + error: RepoNotFound, +}); + +export const cloneToken = HttpApiEndpoint.post( + "cloneToken", + "/repos/:name/clone-token", + { + params: RepoNameParam, + payload: CloneTokenPayload, + success: CloneToken, + error: RepoNotFound, + }, +); + +export class ReposGroup extends HttpApiGroup.make("repos") + .add(createRepo) + .add(getRepo) + .add(updateRepo) + .add(deleteRepo) + .add(starRepo) + .add(cloneToken) {} + +export class RepoApi extends HttpApi.make("RepoApi").add(ReposGroup) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Repo.ts b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Repo.ts new file mode 100644 index 00000000000..2a1c746243e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Repo.ts @@ -0,0 +1,56 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export type Metadata = { + description: string; + topics: string[]; + stars: number; + createdAt: number; +}; + +export default class Repo extends Cloudflare.DurableObjectNamespace()( + "Repo", + Effect.gen(function* () { + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + let meta = (yield* state.storage.get("meta")) ?? null; + + const ensure = Effect.gen(function* () { + if (meta === null) { + return yield* Effect.fail(new Error("repo not initialized")); + } + return meta; + }); + + return { + init: (description: string) => + Effect.gen(function* () { + if (meta !== null) return meta; + meta = { + description, + topics: [], + stars: 0, + createdAt: Date.now(), + }; + yield* state.storage.put("meta", meta); + return meta; + }), + get: () => ensure, + update: (patch: Partial>) => + Effect.gen(function* () { + const current = yield* ensure; + meta = { ...current, ...patch }; + yield* state.storage.put("meta", meta); + return meta; + }), + star: () => + Effect.gen(function* () { + const current = yield* ensure; + meta = { ...current, stars: current.stars + 1 }; + yield* state.storage.put("meta", meta); + return meta; + }), + }; + }); + }), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Repos.ts b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Repos.ts new file mode 100644 index 00000000000..2ed940cd088 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Repos.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const Repos = Cloudflare.Artifacts("Repos"); diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Worker.ts b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Worker.ts new file mode 100644 index 00000000000..37b032d037c --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/src/Worker.ts @@ -0,0 +1,174 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Etag from "effect/unstable/http/Etag"; +import * as HttpPlatform from "effect/unstable/http/HttpPlatform"; +import * as HttpRouter from "effect/unstable/http/HttpRouter"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; +import { + CloneToken, + CreateRepoResponse, + Metadata, + RepoApi, + RepoConflict, + RepoInfo, + RepoNotFound, +} from "./Api.ts"; +import Repo from "./Repo.ts"; +import { Repos } from "./Repos.ts"; + +// Workers don't have a FileSystem, so HttpPlatform's file-response surface +// is stubbed. The repo API never serves files. +const HttpPlatformStub = Layer.succeed(HttpPlatform.HttpPlatform, { + fileResponse: () => Effect.die("HttpPlatform.fileResponse not supported"), + fileWebResponse: () => + Effect.die("HttpPlatform.fileWebResponse not supported"), +}); + +export default class Worker extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + observability: { enabled: true }, + compatibility: { + flags: ["nodejs_compat"], + date: "2026-03-17", + }, + }, + Effect.gen(function* () { + const artifacts = yield* Cloudflare.Artifacts.bind(Repos); + const repos = yield* Repo; + + const findRepo = (name: string) => + artifacts.list({ limit: 100 }).pipe( + Effect.flatMap((res) => { + const found = res.repos.find( + (r: { name: string }) => r.name === name, + ); + return found + ? Effect.succeed(found) + : Effect.fail(new RepoNotFound({ name })); + }), + Effect.catchTag("ArtifactsError", () => + Effect.fail(new RepoNotFound({ name })), + ), + ); + + const handlers = HttpApiBuilder.group(RepoApi, "repos", (h) => + h + .handle("createRepo", ({ payload }) => + artifacts + .create(payload.name, { + description: payload.description, + setDefaultBranch: "main", + }) + .pipe( + Effect.tap(() => + repos + .getByName(payload.name) + .init(payload.description ?? "") + .pipe(Effect.orDie), + ), + Effect.map( + (c) => + new CreateRepoResponse({ + name: c.name, + remote: c.remote, + token: c.token, + defaultBranch: c.defaultBranch, + }), + ), + Effect.catchTag("ArtifactsError", (err) => + Effect.fail(new RepoConflict({ message: err.message })), + ), + ), + ) + .handle("getRepo", ({ params }) => + findRepo(params.name).pipe( + Effect.flatMap((found) => + repos + .getByName(params.name) + .get() + .pipe( + Effect.catch(() => Effect.succeed(null)), + Effect.map( + (meta) => + new RepoInfo({ + id: found.id, + name: found.name, + description: found.description ?? null, + defaultBranch: found.defaultBranch, + remote: found.remote, + status: found.status, + readOnly: found.readOnly, + createdAt: found.createdAt, + updatedAt: found.updatedAt, + lastPushAt: found.lastPushAt ?? null, + metadata: meta ? new Metadata(meta) : null, + }), + ), + ), + ), + ), + ) + .handle("updateRepo", ({ params, payload }) => + findRepo(params.name).pipe( + Effect.flatMap(() => + repos + .getByName(params.name) + .update({ + description: payload.description, + topics: payload.topics ? [...payload.topics] : undefined, + }) + .pipe(Effect.orDie), + ), + Effect.map((m) => new Metadata(m)), + ), + ) + .handle("deleteRepo", ({ params }) => + artifacts.delete(params.name).pipe( + Effect.asVoid, + Effect.catchTag("ArtifactsError", () => + Effect.fail(new RepoNotFound({ name: params.name })), + ), + ), + ) + .handle("starRepo", ({ params }) => + findRepo(params.name).pipe( + Effect.flatMap(() => + repos.getByName(params.name).star().pipe(Effect.orDie), + ), + Effect.map((m) => new Metadata(m)), + ), + ) + .handle("cloneToken", ({ params, payload }) => + artifacts.get(params.name).pipe( + Effect.flatMap((handle) => + handle.createToken(payload.scope ?? "read", payload.ttl ?? 3600), + ), + Effect.map( + (t) => + new CloneToken({ + id: t.id, + plaintext: t.plaintext, + scope: t.scope as "read" | "write", + expiresAt: t.expiresAt, + }), + ), + Effect.catchTag("ArtifactsError", () => + Effect.fail(new RepoNotFound({ name: params.name })), + ), + ), + ), + ); + + return { + fetch: HttpApiBuilder.layer(RepoApi).pipe( + Layer.provide(handlers), + Layer.provide([Etag.layer, HttpPlatformStub, Path.layer]), + HttpRouter.toHttpEffect, + ), + }; + }).pipe(Effect.provide(Layer.mergeAll(Cloudflare.ArtifactsBindingLive))), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/test/integ.test.ts new file mode 100644 index 00000000000..6b9311c343d --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/test/integ.test.ts @@ -0,0 +1,60 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Effect from "effect/Effect"; +import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; +import { RepoApi } from "../src/Api.ts"; +import Stack from "../alchemy.run.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), +}); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +const repoName = `tutorial-${Date.now().toString(36)}`; + +test( + "repo lifecycle", + Effect.gen(function* () { + const { url } = yield* stack; + const client = yield* HttpApiClient.make(RepoApi, { baseUrl: url }); + + const created = yield* client.repos.createRepo({ + payload: { name: repoName, description: "tutorial repo" }, + }); + expect(created.name).toBe(repoName); + expect(created.remote).toBeString(); + expect(created.token).toBeString(); + + const info = yield* client.repos.getRepo({ params: { name: repoName } }); + expect(info.name).toBe(repoName); + expect(info.defaultBranch).toBe("main"); + expect(info.metadata?.description).toBe("tutorial repo"); + expect(info.metadata?.stars).toBe(0); + + const token = yield* client.repos.cloneToken({ + params: { name: repoName }, + payload: { scope: "read", ttl: 600 }, + }); + expect(token.plaintext).toBeString(); + expect(token.scope).toBe("read"); + + const updated = yield* client.repos.updateRepo({ + params: { name: repoName }, + payload: { description: "now with stars", topics: ["demo", "alchemy"] }, + }); + expect(updated.description).toBe("now with stars"); + expect(updated.topics).toEqual(["demo", "alchemy"]); + + const starred = yield* client.repos.starRepo({ + params: { name: repoName }, + }); + expect(starred.stars).toBe(1); + + yield* client.repos.deleteRepo({ params: { name: repoName } }); + }), + { timeout: 120_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-git-artifacts/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/tsconfig.json new file mode 100644 index 00000000000..0cf8c32afd6 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-git-artifacts/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts", "test/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/alchemy.run.ts new file mode 100644 index 00000000000..c8ee9fea832 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/alchemy.run.ts @@ -0,0 +1,32 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import * as Neon from "alchemy/Neon"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import Api from "./src/Api.ts"; +import { Hyperdrive, NeonDb } from "./src/Db.ts"; + +export default Alchemy.Stack( + "CloudflareNeonDrizzleExample", + { + providers: Layer.mergeAll( + Cloudflare.providers(), + Drizzle.providers(), + Neon.providers(), + ), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const { branch } = yield* NeonDb; + const hd = yield* Hyperdrive; + const api = yield* Api; + + return { + url: api.url.as(), + branchId: branch.branchId, + hyperdriveId: hd.hyperdriveId, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/migrations/20260504073935_migration/migration.sql b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/migrations/20260504073935_migration/migration.sql new file mode 100644 index 00000000000..73388003512 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/migrations/20260504073935_migration/migration.sql @@ -0,0 +1,18 @@ +CREATE TABLE "posts" ( + "id" serial PRIMARY KEY, + "user_id" integer NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); + +--> statement-breakpoint +CREATE TABLE "users" ( + "id" serial PRIMARY KEY, + "email" text NOT NULL UNIQUE, + "name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); + +--> statement-breakpoint +ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE; diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/migrations/20260504073935_migration/snapshot.json b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/migrations/20260504073935_migration/snapshot.json new file mode 100644 index 00000000000..08028870b8a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/migrations/20260504073935_migration/snapshot.json @@ -0,0 +1,176 @@ +{ + "dialect": "postgres", + "id": "2e3d1fe8-2425-4248-b808-e2055d532265", + "prevIds": ["00000000-0000-0000-0000-000000000000"], + "version": "8", + "ddl": [ + { + "isRlsEnabled": false, + "name": "posts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "posts_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "posts" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "posts_pkey", + "schema": "public", + "table": "posts", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + } + ], + "renames": [] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/package.json b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/package.json new file mode 100644 index 00000000000..51e2b8afc14 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/package.json @@ -0,0 +1,32 @@ +{ + "name": "cloudflare-neon-drizzle", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-neon-drizzle" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "test": "bun test" + }, + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@effect/sql-pg": "catalog:", + "alchemy": "workspace:*", + "drizzle-orm": "1.0.0-rc.1", + "effect": "catalog:", + "pg": "^8.13.0" + }, + "devDependencies": { + "@types/pg": "^8.11.0", + "drizzle-kit": "1.0.0-rc.1" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Api.ts b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Api.ts new file mode 100644 index 00000000000..67c32797600 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Api.ts @@ -0,0 +1,92 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import { eq } from "drizzle-orm"; +import { Layer } from "effect"; +import * as Effect from "effect/Effect"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Hyperdrive } from "./Db.ts"; +import { relations, Users } from "./schema.ts"; + +export default class Api extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const conn = yield* Cloudflare.Hyperdrive.bind(Hyperdrive); + const db = yield* Drizzle.postgres(conn.connectionString, { + relations, + }); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + switch (request.method) { + case "GET": { + if (request.url === "/") { + const users = yield* db.select().from(Users); + return yield* HttpServerResponse.json({ users }); + } + const id = Number(request.url.split("/").pop()); + if (Number.isNaN(id)) { + return yield* HttpServerResponse.json( + { error: "Invalid user ID" }, + { status: 400 }, + ); + } + const user = yield* db.query.Users.findFirst({ + where: { id }, + with: { posts: true }, + }); + return yield* HttpServerResponse.json({ user }); + } + case "POST": { + const user = yield* db + .insert(Users) + .values({ + name: crypto.randomUUID(), + email: crypto.randomUUID(), + }) + .returning(); + return yield* HttpServerResponse.json({ user }); + } + case "DELETE": { + const id = Number(request.url.split("/").pop()); + if (Number.isNaN(id)) { + return yield* HttpServerResponse.json( + { error: "Invalid user ID" }, + { status: 400 }, + ); + } + const [user] = yield* db + .delete(Users) + .where(eq(Users.id, id)) + .returning(); + return yield* HttpServerResponse.json({ user }); + } + default: { + return yield* HttpServerResponse.json( + { error: "Method not allowed" }, + { status: 405 }, + ); + } + } + }).pipe( + Effect.catch((cause: any) => { + const peel = (e: any): any => (e?.cause ? peel(e.cause) : e); + const root = peel(cause); + return HttpServerResponse.json( + { + ok: false, + error: String(cause), + rootError: root?.message ?? String(root), + rootCode: root?.code, + }, + { status: 500 }, + ); + }), + ), + }; + }).pipe(Effect.provide(Layer.mergeAll(Cloudflare.HyperdriveBindingLive))), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Db.ts b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Db.ts new file mode 100644 index 00000000000..d18d0b1b5b5 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Db.ts @@ -0,0 +1,43 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import * as Neon from "alchemy/Neon"; +import * as Effect from "effect/Effect"; + +/** + * A Drizzle schema + Neon project + feature branch. The branch's + * `migrationsDir` is wired to the schema resource's `out` output, so the + * provider order becomes: + * + * 1. `Drizzle.Schema` regenerates pending migration SQL files. + * 2. `Neon.Branch` scans the directory and applies any new migrations + * transactionally. + */ +export const NeonDb = Effect.gen(function* () { + const { stage } = yield* Alchemy.Stack; + + const schema = yield* Drizzle.Schema("app-schema", { + schema: "./src/schema.ts", + out: "./migrations", + }); + + const project = stage.startsWith("pr-") + ? yield* Neon.Project.ref("app-db", { stage: `staging-${stage}` }) + : yield* Neon.Project("app-db", { + region: "aws-us-east-1", + }); + + const branch = yield* Neon.Branch("app-branch", { + project, + migrationsDir: schema.out, + }); + + return { project, branch, schema }; +}); + +export const Hyperdrive = Effect.gen(function* () { + const { branch } = yield* NeonDb; + return yield* Cloudflare.Hyperdrive("app-hyperdrive", { + origin: branch.origin, + }); +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/schema.ts b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/schema.ts new file mode 100644 index 00000000000..7c3633eedda --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/schema.ts @@ -0,0 +1,37 @@ +import { defineRelations } from "drizzle-orm"; +import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; + +export const Users = pgTable("users", { + id: serial("id").primaryKey(), + email: text("email").notNull().unique(), + name: text("name").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +export type User = typeof Users.$inferSelect; + +export const Posts = pgTable("posts", { + id: serial("id").primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => Users.id, { onDelete: "cascade" }), + title: text("title").notNull(), + body: text("body").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +export type Post = typeof Posts.$inferSelect; + +export const relations = defineRelations({ Users, Posts }, (t) => ({ + Users: { + posts: t.many.Posts(), + }, + Posts: { + user: t.one.Users({ + from: t.Posts.userId, + to: t.Users.id, + }), + }, +})); diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/test/integ.test.ts new file mode 100644 index 00000000000..c0ea4a1c3e2 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/test/integ.test.ts @@ -0,0 +1,164 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import * as Neon from "alchemy/Neon"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "../alchemy.run.ts"; +import type { Post, User } from "../src/schema.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Layer.mergeAll( + Cloudflare.providers(), + Drizzle.providers(), + Neon.providers(), + ), + state: Alchemy.localState(), +}); + +const stack = beforeAll(deploy(Stack)); + +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "worker exposes a URL, hyperdrive id, and neon branch id", + Effect.gen(function* () { + const { url, branchId, hyperdriveId } = yield* stack; + + expect(url).toBeString(); + expect(branchId).toBeString(); + expect(hyperdriveId).toBeString(); + }), +); + +// workers.dev subdomain takes a few seconds to propagate after first +// enable; retry until the worker actually answers. +const getOnce = (url: string) => + Effect.gen(function* () { + const response = yield* HttpClient.get(url); + if (response.status === 404) { + return yield* Effect.fail(new Error("workers.dev not yet propagated")); + } + return response; + }).pipe( + Effect.tapError((err) => + Effect.logError(`${url} not available: ${err.message}`), + ), + Effect.retry({ schedule: Schedule.spaced("1 second"), times: 30 }), + ); + +test( + "worker exposes user CRUD through Drizzle / Hyperdrive / Neon", + Effect.gen(function* () { + const { url } = yield* stack; + const baseUrl = url.replace(/\/+$/, ""); + + const initialResponse = yield* getOnce(baseUrl); + expect(initialResponse.status).toBe(200); + + const initialBody = (yield* initialResponse.json) as unknown as { + users: User[]; + }; + expect(Array.isArray(initialBody.users)).toBe(true); + + const createResponse = yield* HttpClient.execute( + HttpClientRequest.post(baseUrl), + ); + expect(createResponse.status).toBe(200); + + const createBody = (yield* createResponse.json) as unknown as { + user: User[]; + }; + expect(createBody.user).toHaveLength(1); + + const [createdUser] = createBody.user; + expect(createdUser.id).toBeNumber(); + expect(createdUser.email).toBeString(); + expect(createdUser.name).toBeString(); + expect(createdUser.createdAt).toBeString(); + + const readResponse = yield* HttpClient.get(`${baseUrl}/${createdUser.id}`); + expect(readResponse.status).toBe(200); + + const readBody = (yield* readResponse.json) as unknown as { + user: User & { posts: Post[] }; + }; + expect(readBody.user).toMatchObject({ + id: createdUser.id, + email: createdUser.email, + name: createdUser.name, + posts: [], + }); + + const invalidReadResponse = yield* HttpClient.get(`${baseUrl}/not-a-user`); + expect(invalidReadResponse.status).toBe(400); + expect(yield* invalidReadResponse.json).toEqual({ + error: "Invalid user ID", + }); + + const methodResponse = yield* HttpClient.execute( + HttpClientRequest.patch(baseUrl), + ); + expect(methodResponse.status).toBe(405); + expect(yield* methodResponse.json).toEqual({ + error: "Method not allowed", + }); + + const deleteResponse = yield* HttpClient.execute( + HttpClientRequest.delete(`${baseUrl}/${createdUser.id}`), + ); + expect(deleteResponse.status).toBe(200); + + const deleteBody = (yield* deleteResponse.json) as unknown as { + user: User; + }; + expect(deleteBody.user).toMatchObject({ + id: createdUser.id, + email: createdUser.email, + name: createdUser.name, + }); + + const finalResponse = yield* HttpClient.get(baseUrl); + expect(finalResponse.status).toBe(200); + const finalBody = (yield* finalResponse.json) as unknown as { + users: User[]; + }; + expect(finalBody.users.some((user) => user.id === createdUser.id)).toBe( + false, + ); + }), + { timeout: 20_000 }, +); + +test( + "worker handles 100 sequential queries spaced 100-500ms apart", + Effect.gen(function* () { + const { url } = yield* stack; + const baseUrl = url.replace(/\/+$/, ""); + + yield* getOnce(baseUrl); + + const queryOnce = Effect.gen(function* () { + const response = yield* HttpClient.get(baseUrl); + expect(response.status).toBe(200); + const body = (yield* response.json) as unknown as { users: User[] }; + expect(Array.isArray(body.users)).toBe(true); + }); + + const jitter = Effect.sync( + () => Math.floor(Math.random() * 401) + 100, + ).pipe(Effect.flatMap((ms) => Effect.sleep(Duration.millis(ms)))); + + yield* queryOnce.pipe( + Effect.zip(jitter), + Effect.repeat(Schedule.recurs(99)), + ); + }), + { timeout: 120_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/tsconfig.json new file mode 100644 index 00000000000..fc740bccbce --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-neon-drizzle/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "alchemy.run.ts", + "src/**/*.ts", + "scripts/**/*.ts", + "test/**/*.ts" + ], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/alchemy.run.ts new file mode 100644 index 00000000000..1a06c343879 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/alchemy.run.ts @@ -0,0 +1,28 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Planetscale from "alchemy/Planetscale"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import Api from "./src/Api.ts"; +import { Hyperdrive, PlanetscaleDb } from "./src/Db.ts"; + +export default Alchemy.Stack( + "CloudflarePlanetscaleMySQLDrizzleExample", + { + providers: Layer.mergeAll(Cloudflare.providers(), Planetscale.providers()), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const { database, branch } = yield* PlanetscaleDb; + const hd = yield* Hyperdrive; + const api = yield* Api; + + return { + url: api.url.as(), + databaseId: database.id, + branchName: branch.name, + hyperdriveId: hd.hyperdriveId, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/drizzle.config.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/drizzle.config.ts new file mode 100644 index 00000000000..be71282ee76 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/schema.ts", + out: "./migrations", + dialect: "mysql", +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/migrations/20260519110126_dazzling_may_parker/migration.sql b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/migrations/20260519110126_dazzling_may_parker/migration.sql new file mode 100644 index 00000000000..00781153d1d --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/migrations/20260519110126_dazzling_may_parker/migration.sql @@ -0,0 +1,15 @@ +CREATE TABLE `posts` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `user_id` int NOT NULL, + `title` varchar(255) NOT NULL, + `body` varchar(4096) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT (now()) +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `email` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `email_unique` UNIQUE INDEX(`email`) +); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/migrations/20260519110126_dazzling_may_parker/snapshot.json b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/migrations/20260519110126_dazzling_may_parker/snapshot.json new file mode 100644 index 00000000000..286bb796710 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/migrations/20260519110126_dazzling_may_parker/snapshot.json @@ -0,0 +1,171 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "34f6e5bd-9164-4ff3-b298-bf82949627f3", + "prevIds": ["00000000-0000-0000-0000-000000000000"], + "ddl": [ + { + "name": "posts", + "entityType": "tables" + }, + { + "name": "users", + "entityType": "tables" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": true, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "posts" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "posts" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "posts" + }, + { + "type": "varchar(4096)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "body", + "entityType": "columns", + "table": "posts" + }, + { + "type": "timestamp", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "posts" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": true, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "users" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "users" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "users" + }, + { + "type": "timestamp", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "users" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "posts", + "entityType": "pks" + }, + { + "columns": ["id"], + "name": "PRIMARY", + "table": "users", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": false, + "name": "email_unique", + "table": "users", + "entityType": "indexes" + } + ], + "renames": [] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/package.json b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/package.json new file mode 100644 index 00000000000..ca2cf229241 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/package.json @@ -0,0 +1,31 @@ +{ + "name": "cloudflare-planetscale-mysql-drizzle", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-planetscale-mysql-drizzle" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "generate:migrations": "drizzle-kit generate", + "test": "bun test" + }, + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "drizzle-orm": "1.0.0-rc.1", + "effect": "catalog:", + "mysql2": "^3.15.3" + }, + "devDependencies": { + "drizzle-kit": "1.0.0-rc.1" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/Api.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/Api.ts new file mode 100644 index 00000000000..027871f005d --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/Api.ts @@ -0,0 +1,129 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import { eq } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/mysql2"; +import { Layer } from "effect"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Hyperdrive } from "./Db.ts"; +import * as schema from "./schema.ts"; +import { relations, Users } from "./schema.ts"; + +export default class Api extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + compatibility: { + date: "2026-03-17", + flags: ["nodejs_compat"], + }, + }, + Effect.gen(function* () { + const conn = yield* Cloudflare.Hyperdrive.bind(Hyperdrive); + + return { + fetch: Effect.gen(function* () { + const connectionString = yield* conn.connectionString; + const db = drizzle(Redacted.value(connectionString), { + schema, + relations, + mode: "default", + }); + + const close = Effect.tryPromise({ + try: () => db.$client.end(), + catch: (cause) => cause, + }).pipe(Effect.catch(() => Effect.void)); + + const request = yield* HttpServerRequest.HttpServerRequest; + return yield* Effect.gen(function* () { + switch (request.method) { + case "GET": { + if (request.url === "/") { + const users = yield* Effect.tryPromise({ + try: () => db.select().from(Users), + catch: (cause) => cause, + }); + return yield* HttpServerResponse.json({ users }); + } + const id = Number(request.url.split("/").pop()); + if (Number.isNaN(id)) { + return yield* HttpServerResponse.json( + { error: "Invalid user ID" }, + { status: 400 }, + ); + } + const user = yield* Effect.tryPromise({ + try: () => + db.query.Users.findFirst({ + where: { id }, + with: { posts: true }, + }), + catch: (cause) => cause, + }); + return yield* HttpServerResponse.json({ user }); + } + case "POST": { + const [created] = yield* Effect.tryPromise({ + try: () => + db + .insert(Users) + .values({ + name: crypto.randomUUID(), + email: crypto.randomUUID(), + }) + .$returningId(), + catch: (cause) => cause, + }); + const user = yield* Effect.tryPromise({ + try: () => + db.select().from(Users).where(eq(Users.id, created.id)), + catch: (cause) => cause, + }); + return yield* HttpServerResponse.json({ user }); + } + case "DELETE": { + const id = Number(request.url.split("/").pop()); + if (Number.isNaN(id)) { + return yield* HttpServerResponse.json( + { error: "Invalid user ID" }, + { status: 400 }, + ); + } + const [user] = yield* Effect.tryPromise({ + try: () => db.select().from(Users).where(eq(Users.id, id)), + catch: (cause) => cause, + }); + yield* Effect.tryPromise({ + try: () => db.delete(Users).where(eq(Users.id, id)), + catch: (cause) => cause, + }); + return yield* HttpServerResponse.json({ user }); + } + default: { + return yield* HttpServerResponse.json( + { error: "Method not allowed" }, + { status: 405 }, + ); + } + } + }).pipe(Effect.ensuring(close)); + }).pipe( + Effect.catch((cause: any) => { + const peel = (e: any): any => (e?.cause ? peel(e.cause) : e); + const root = peel(cause); + return HttpServerResponse.json( + { + ok: false, + error: String(cause), + rootError: root?.message ?? String(root), + rootCode: root?.code, + }, + { status: 500 }, + ); + }), + ), + }; + }).pipe(Effect.provide(Layer.mergeAll(Cloudflare.HyperdriveBindingLive))), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/Db.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/Db.ts new file mode 100644 index 00000000000..8036369945a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/Db.ts @@ -0,0 +1,53 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Planetscale from "alchemy/Planetscale"; +import * as Effect from "effect/Effect"; + +/** + * A Drizzle schema + PlanetScale MySQL database + feature branch. Generate + * migrations with `bun generate:migrations`; the branch scans the checked-in + * migration directory and applies new files transactionally. + */ +export const PlanetscaleDb = Effect.gen(function* () { + const { stage } = yield* Alchemy.Stack; + + // Stages are organised in two tiers: + // + // - `staging-*` stages own the long-lived PlanetScale database. + // - `pr-*` stages reference the parallel `staging-pr-*` database + // and only own ephemeral compute (branch + role + Hyperdrive + Worker). + // + // Deriving `staging-${stage}` instead of a single global "staging" + // keeps each test / PR isolated. Locally (`dev_`, etc.) we create + // a fresh database. + const database = stage.startsWith("pr-") + ? yield* Planetscale.MySQLDatabase.ref("app-db", { + stage: `staging-${stage}`, + }) + : yield* Planetscale.MySQLDatabase("app-db", { + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.MySQLBranch("app-branch", { + database, + isProduction: false, + migrationsDir: "./migrations", + }); + + const password = yield* Planetscale.MySQLPassword("app-password", { + database, + branch, + role: "readwriter", + }); + + return { database, branch, password }; +}); + +export const Hyperdrive: Effect.Effect = + Effect.gen(function* () { + const { password } = yield* PlanetscaleDb; + return yield* Cloudflare.Hyperdrive("app-hyperdrive", { + origin: password.origin, + }); + }); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/schema.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/schema.ts new file mode 100644 index 00000000000..dd5d96f6175 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/src/schema.ts @@ -0,0 +1,31 @@ +import { defineRelations } from "drizzle-orm"; +import { int, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core"; + +export const Users = mysqlTable("users", { + id: int("id").primaryKey().autoincrement(), + email: varchar("email", { length: 255 }).notNull().unique(), + name: varchar("name", { length: 255 }).notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}); +export type User = typeof Users.$inferSelect; + +export const Posts = mysqlTable("posts", { + id: int("id").primaryKey().autoincrement(), + userId: int("user_id").notNull(), + title: varchar("title", { length: 255 }).notNull(), + body: varchar("body", { length: 4096 }).notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}); +export type Post = typeof Posts.$inferSelect; + +export const relations = defineRelations({ Users, Posts }, (t) => ({ + Users: { + posts: t.many.Posts(), + }, + Posts: { + user: t.one.Users({ + from: t.Posts.userId, + to: t.Users.id, + }), + }, +})); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/test/integ.test.ts new file mode 100644 index 00000000000..861aac56ed9 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/test/integ.test.ts @@ -0,0 +1,160 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Planetscale from "alchemy/Planetscale"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "../alchemy.run.ts"; +import type { Post, User } from "../src/schema.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Layer.mergeAll(Cloudflare.providers(), Planetscale.providers()), + state: Alchemy.localState(), +}); + +const stack = beforeAll(deploy(Stack)); + +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "worker exposes a URL, hyperdrive id, and planetscale identifiers", + Effect.gen(function* () { + const { url, databaseId, branchName, hyperdriveId } = yield* stack; + + expect(url).toBeString(); + expect(databaseId).toBeString(); + expect(branchName).toBeString(); + expect(hyperdriveId).toBeString(); + }), +); + +// workers.dev subdomain takes a few seconds to propagate after first +// enable; retry until the worker actually answers. +const getOnce = (url: string) => + Effect.gen(function* () { + const response = yield* HttpClient.get(url); + if (response.status === 404) { + return yield* Effect.fail(new Error("workers.dev not yet propagated")); + } + return response; + }).pipe( + Effect.tapError((err) => + Effect.logError(`${url} not available: ${err.message}`), + ), + Effect.retry({ schedule: Schedule.spaced("1 second"), times: 30 }), + ); + +test( + "worker exposes user CRUD through Drizzle / Hyperdrive / PlanetScale", + Effect.gen(function* () { + const { url } = yield* stack; + const baseUrl = url.replace(/\/+$/, ""); + + const initialResponse = yield* getOnce(baseUrl); + expect(initialResponse.status).toBe(200); + + const initialBody = (yield* initialResponse.json) as unknown as { + users: User[]; + }; + expect(Array.isArray(initialBody.users)).toBe(true); + + const createResponse = yield* HttpClient.execute( + HttpClientRequest.post(baseUrl), + ); + expect(createResponse.status).toBe(200); + + const createBody = (yield* createResponse.json) as unknown as { + user: User[]; + }; + expect(createBody.user).toHaveLength(1); + + const [createdUser] = createBody.user; + expect(createdUser.id).toBeNumber(); + expect(createdUser.email).toBeString(); + expect(createdUser.name).toBeString(); + expect(createdUser.createdAt).toBeString(); + + const readResponse = yield* HttpClient.get(`${baseUrl}/${createdUser.id}`); + expect(readResponse.status).toBe(200); + + const readBody = (yield* readResponse.json) as unknown as { + user: User & { posts: Post[] }; + }; + expect(readBody.user).toMatchObject({ + id: createdUser.id, + email: createdUser.email, + name: createdUser.name, + posts: [], + }); + + const invalidReadResponse = yield* HttpClient.get(`${baseUrl}/not-a-user`); + expect(invalidReadResponse.status).toBe(400); + expect(yield* invalidReadResponse.json).toEqual({ + error: "Invalid user ID", + }); + + const methodResponse = yield* HttpClient.execute( + HttpClientRequest.patch(baseUrl), + ); + expect(methodResponse.status).toBe(405); + expect(yield* methodResponse.json).toEqual({ + error: "Method not allowed", + }); + + const deleteResponse = yield* HttpClient.execute( + HttpClientRequest.delete(`${baseUrl}/${createdUser.id}`), + ); + expect(deleteResponse.status).toBe(200); + + const deleteBody = (yield* deleteResponse.json) as unknown as { + user: User; + }; + expect(deleteBody.user).toMatchObject({ + id: createdUser.id, + email: createdUser.email, + name: createdUser.name, + }); + + const finalResponse = yield* HttpClient.get(baseUrl); + expect(finalResponse.status).toBe(200); + const finalBody = (yield* finalResponse.json) as unknown as { + users: User[]; + }; + expect(finalBody.users.some((user) => user.id === createdUser.id)).toBe( + false, + ); + }), + { timeout: 20_000 }, +); + +test( + "worker handles 100 sequential queries spaced 100-500ms apart", + Effect.gen(function* () { + const { url } = yield* stack; + const baseUrl = url.replace(/\/+$/, ""); + + yield* getOnce(baseUrl); + + const queryOnce = Effect.gen(function* () { + const response = yield* HttpClient.get(baseUrl); + expect(response.status).toBe(200); + const body = (yield* response.json) as unknown as { users: User[] }; + expect(Array.isArray(body.users)).toBe(true); + }); + + const jitter = Effect.sync( + () => Math.floor(Math.random() * 401) + 100, + ).pipe(Effect.flatMap((ms) => Effect.sleep(Duration.millis(ms)))); + + yield* queryOnce.pipe( + Effect.zip(jitter), + Effect.repeat(Schedule.recurs(99)), + ); + }), + { timeout: 120_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/tsconfig.json new file mode 100644 index 00000000000..bb339a5b2dd --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-mysql-drizzle/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "alchemy.run.ts", + "drizzle.config.ts", + "src/**/*.ts", + "scripts/**/*.ts", + "test/**/*.ts" + ], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "declaration": false, + "declarationMap": false + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/alchemy.run.ts new file mode 100644 index 00000000000..5d01b182093 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/alchemy.run.ts @@ -0,0 +1,33 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import * as Planetscale from "alchemy/Planetscale"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import Api from "./src/Api.ts"; +import { Hyperdrive, PlanetscaleDb } from "./src/Db.ts"; + +export default Alchemy.Stack( + "CloudflarePlanetscalePostgresDrizzleExample", + { + providers: Layer.mergeAll( + Cloudflare.providers(), + Drizzle.providers(), + Planetscale.providers(), + ), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const { database, branch } = yield* PlanetscaleDb; + const hd = yield* Hyperdrive; + const api = yield* Api; + + return { + url: api.url.as(), + databaseId: database.id, + branchName: branch.name, + hyperdriveId: hd.hyperdriveId, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/migrations/20260504073935_migration/migration.sql b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/migrations/20260504073935_migration/migration.sql new file mode 100644 index 00000000000..73388003512 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/migrations/20260504073935_migration/migration.sql @@ -0,0 +1,18 @@ +CREATE TABLE "posts" ( + "id" serial PRIMARY KEY, + "user_id" integer NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); + +--> statement-breakpoint +CREATE TABLE "users" ( + "id" serial PRIMARY KEY, + "email" text NOT NULL UNIQUE, + "name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); + +--> statement-breakpoint +ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE; diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/migrations/20260504073935_migration/snapshot.json b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/migrations/20260504073935_migration/snapshot.json new file mode 100644 index 00000000000..08028870b8a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/migrations/20260504073935_migration/snapshot.json @@ -0,0 +1,176 @@ +{ + "dialect": "postgres", + "id": "2e3d1fe8-2425-4248-b808-e2055d532265", + "prevIds": ["00000000-0000-0000-0000-000000000000"], + "version": "8", + "ddl": [ + { + "isRlsEnabled": false, + "name": "posts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "posts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "posts_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "posts" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "posts_pkey", + "schema": "public", + "table": "posts", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + } + ], + "renames": [] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/package.json b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/package.json new file mode 100644 index 00000000000..76da4069c42 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/package.json @@ -0,0 +1,32 @@ +{ + "name": "cloudflare-planetscale-postgres-drizzle", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-planetscale-postgres-drizzle" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "test": "bun test" + }, + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@effect/sql-pg": "catalog:", + "alchemy": "workspace:*", + "drizzle-orm": "1.0.0-rc.1", + "effect": "catalog:", + "pg": "^8.13.0" + }, + "devDependencies": { + "@types/pg": "^8.11.0", + "drizzle-kit": "1.0.0-rc.1" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/Api.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/Api.ts new file mode 100644 index 00000000000..67c32797600 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/Api.ts @@ -0,0 +1,92 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import { eq } from "drizzle-orm"; +import { Layer } from "effect"; +import * as Effect from "effect/Effect"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Hyperdrive } from "./Db.ts"; +import { relations, Users } from "./schema.ts"; + +export default class Api extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const conn = yield* Cloudflare.Hyperdrive.bind(Hyperdrive); + const db = yield* Drizzle.postgres(conn.connectionString, { + relations, + }); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + switch (request.method) { + case "GET": { + if (request.url === "/") { + const users = yield* db.select().from(Users); + return yield* HttpServerResponse.json({ users }); + } + const id = Number(request.url.split("/").pop()); + if (Number.isNaN(id)) { + return yield* HttpServerResponse.json( + { error: "Invalid user ID" }, + { status: 400 }, + ); + } + const user = yield* db.query.Users.findFirst({ + where: { id }, + with: { posts: true }, + }); + return yield* HttpServerResponse.json({ user }); + } + case "POST": { + const user = yield* db + .insert(Users) + .values({ + name: crypto.randomUUID(), + email: crypto.randomUUID(), + }) + .returning(); + return yield* HttpServerResponse.json({ user }); + } + case "DELETE": { + const id = Number(request.url.split("/").pop()); + if (Number.isNaN(id)) { + return yield* HttpServerResponse.json( + { error: "Invalid user ID" }, + { status: 400 }, + ); + } + const [user] = yield* db + .delete(Users) + .where(eq(Users.id, id)) + .returning(); + return yield* HttpServerResponse.json({ user }); + } + default: { + return yield* HttpServerResponse.json( + { error: "Method not allowed" }, + { status: 405 }, + ); + } + } + }).pipe( + Effect.catch((cause: any) => { + const peel = (e: any): any => (e?.cause ? peel(e.cause) : e); + const root = peel(cause); + return HttpServerResponse.json( + { + ok: false, + error: String(cause), + rootError: root?.message ?? String(root), + rootCode: root?.code, + }, + { status: 500 }, + ); + }), + ), + }; + }).pipe(Effect.provide(Layer.mergeAll(Cloudflare.HyperdriveBindingLive))), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/Db.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/Db.ts new file mode 100644 index 00000000000..b537cb7ec27 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/Db.ts @@ -0,0 +1,62 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import * as Planetscale from "alchemy/Planetscale"; +import * as Effect from "effect/Effect"; + +/** + * A Drizzle schema + PlanetScale Postgres database + feature branch. The + * branch's `migrationsDir` is wired to the schema resource's `out` output, so + * the provider order becomes: + * + * 1. `Drizzle.Schema` regenerates pending migration SQL files. + * 2. `Planetscale.PostgresBranch` scans the directory and applies new + * migrations transactionally. + */ +export const PlanetscaleDb = Effect.gen(function* () { + const { stage } = yield* Alchemy.Stack; + + const schema = yield* Drizzle.Schema("app-schema", { + schema: "./src/schema.ts", + out: "./migrations", + }); + + // Stages are organised in two tiers: + // + // - `staging-*` stages own the long-lived PlanetScale database. + // - `pr-*` stages reference the parallel `staging-pr-*` database + // and only own ephemeral compute (branch + role + Hyperdrive + Worker). + // + // Deriving `staging-${stage}` instead of a single global "staging" + // keeps each test / PR isolated. Locally (`dev_`, etc.) we create + // a fresh database. + const database = stage.startsWith("pr-") + ? yield* Planetscale.PostgresDatabase.ref("app-db", { + stage: `staging-${stage}`, + }) + : yield* Planetscale.PostgresDatabase("app-db", { + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.PostgresBranch("app-branch", { + database, + migrationsDir: schema.out, + }); + + const role = yield* Planetscale.PostgresRole("app-role", { + database, + branch, + inheritedRoles: ["postgres"], + }); + + return { database, branch, role, schema }; +}); + +export const Hyperdrive: Effect.Effect = + Effect.gen(function* () { + const { role } = yield* PlanetscaleDb; + return yield* Cloudflare.Hyperdrive("app-hyperdrive", { + origin: role.origin, + }); + }); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/schema.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/schema.ts new file mode 100644 index 00000000000..7c3633eedda --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/src/schema.ts @@ -0,0 +1,37 @@ +import { defineRelations } from "drizzle-orm"; +import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; + +export const Users = pgTable("users", { + id: serial("id").primaryKey(), + email: text("email").notNull().unique(), + name: text("name").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +export type User = typeof Users.$inferSelect; + +export const Posts = pgTable("posts", { + id: serial("id").primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => Users.id, { onDelete: "cascade" }), + title: text("title").notNull(), + body: text("body").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +export type Post = typeof Posts.$inferSelect; + +export const relations = defineRelations({ Users, Posts }, (t) => ({ + Users: { + posts: t.many.Posts(), + }, + Posts: { + user: t.one.Users({ + from: t.Posts.userId, + to: t.Users.id, + }), + }, +})); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/test/integ.test.ts new file mode 100644 index 00000000000..648ac232ed9 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/test/integ.test.ts @@ -0,0 +1,165 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Drizzle from "alchemy/Drizzle"; +import * as Planetscale from "alchemy/Planetscale"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "../alchemy.run.ts"; +import type { Post, User } from "../src/schema.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Layer.mergeAll( + Cloudflare.providers(), + Drizzle.providers(), + Planetscale.providers(), + ), + state: Alchemy.localState(), +}); + +const stack = beforeAll(deploy(Stack)); + +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "worker exposes a URL, hyperdrive id, and planetscale identifiers", + Effect.gen(function* () { + const { url, databaseId, branchName, hyperdriveId } = yield* stack; + + expect(url).toBeString(); + expect(databaseId).toBeString(); + expect(branchName).toBeString(); + expect(hyperdriveId).toBeString(); + }), +); + +// workers.dev subdomain takes a few seconds to propagate after first +// enable; retry until the worker actually answers. +const getOnce = (url: string) => + Effect.gen(function* () { + const response = yield* HttpClient.get(url); + if (response.status === 404) { + return yield* Effect.fail(new Error("workers.dev not yet propagated")); + } + return response; + }).pipe( + Effect.tapError((err) => + Effect.logError(`${url} not available: ${err.message}`), + ), + Effect.retry({ schedule: Schedule.spaced("1 second"), times: 30 }), + ); + +test( + "worker exposes user CRUD through Drizzle / Hyperdrive / PlanetScale", + Effect.gen(function* () { + const { url } = yield* stack; + const baseUrl = url.replace(/\/+$/, ""); + + const initialResponse = yield* getOnce(baseUrl); + expect(initialResponse.status).toBe(200); + + const initialBody = (yield* initialResponse.json) as unknown as { + users: User[]; + }; + expect(Array.isArray(initialBody.users)).toBe(true); + + const createResponse = yield* HttpClient.execute( + HttpClientRequest.post(baseUrl), + ); + expect(createResponse.status).toBe(200); + + const createBody = (yield* createResponse.json) as unknown as { + user: User[]; + }; + expect(createBody.user).toHaveLength(1); + + const [createdUser] = createBody.user; + expect(createdUser.id).toBeNumber(); + expect(createdUser.email).toBeString(); + expect(createdUser.name).toBeString(); + expect(createdUser.createdAt).toBeString(); + + const readResponse = yield* HttpClient.get(`${baseUrl}/${createdUser.id}`); + expect(readResponse.status).toBe(200); + + const readBody = (yield* readResponse.json) as unknown as { + user: User & { posts: Post[] }; + }; + expect(readBody.user).toMatchObject({ + id: createdUser.id, + email: createdUser.email, + name: createdUser.name, + posts: [], + }); + + const invalidReadResponse = yield* HttpClient.get(`${baseUrl}/not-a-user`); + expect(invalidReadResponse.status).toBe(400); + expect(yield* invalidReadResponse.json).toEqual({ + error: "Invalid user ID", + }); + + const methodResponse = yield* HttpClient.execute( + HttpClientRequest.patch(baseUrl), + ); + expect(methodResponse.status).toBe(405); + expect(yield* methodResponse.json).toEqual({ + error: "Method not allowed", + }); + + const deleteResponse = yield* HttpClient.execute( + HttpClientRequest.delete(`${baseUrl}/${createdUser.id}`), + ); + expect(deleteResponse.status).toBe(200); + + const deleteBody = (yield* deleteResponse.json) as unknown as { + user: User; + }; + expect(deleteBody.user).toMatchObject({ + id: createdUser.id, + email: createdUser.email, + name: createdUser.name, + }); + + const finalResponse = yield* HttpClient.get(baseUrl); + expect(finalResponse.status).toBe(200); + const finalBody = (yield* finalResponse.json) as unknown as { + users: User[]; + }; + expect(finalBody.users.some((user) => user.id === createdUser.id)).toBe( + false, + ); + }), + { timeout: 20_000 }, +); + +test( + "worker handles 100 sequential queries spaced 100-500ms apart", + Effect.gen(function* () { + const { url } = yield* stack; + const baseUrl = url.replace(/\/+$/, ""); + + yield* getOnce(baseUrl); + + const queryOnce = Effect.gen(function* () { + const response = yield* HttpClient.get(baseUrl); + expect(response.status).toBe(200); + const body = (yield* response.json) as unknown as { users: User[] }; + expect(Array.isArray(body.users)).toBe(true); + }); + + const jitter = Effect.sync( + () => Math.floor(Math.random() * 401) + 100, + ).pipe(Effect.flatMap((ms) => Effect.sleep(Duration.millis(ms)))); + + yield* queryOnce.pipe( + Effect.zip(jitter), + Effect.repeat(Schedule.recurs(99)), + ); + }), + { timeout: 120_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/tsconfig.json new file mode 100644 index 00000000000..00212290cd7 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-planetscale-postgres-drizzle/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "alchemy.run.ts", + "src/**/*.ts", + "scripts/**/*.ts", + "test/**/*.ts" + ], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "declaration": false, + "declarationMap": false + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-secrets-store/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-secrets-store/alchemy.run.ts new file mode 100644 index 00000000000..1dac38d35c6 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-secrets-store/alchemy.run.ts @@ -0,0 +1,23 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +import Api from "./src/Api.ts"; +import { Store } from "./src/Store.ts"; + +export default Alchemy.Stack( + "CloudflareSecretsStoreExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const store = yield* Store; + const api = yield* Api; + + return { + url: api.url.as(), + storeId: store.storeId, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-secrets-store/package.json b/.repos/alchemy-effect/examples/cloudflare-secrets-store/package.json new file mode 100644 index 00000000000..ed81c75ac1d --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-secrets-store/package.json @@ -0,0 +1,24 @@ +{ + "name": "cloudflare-secrets-store", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-secrets-store" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/Api.ts b/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/Api.ts new file mode 100644 index 00000000000..7500b1b9133 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/Api.ts @@ -0,0 +1,40 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { ApiKey } from "./ApiKey.ts"; + +export default class Api extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const apiKey = yield* Cloudflare.Secret.bind(ApiKey); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + + if (request.url === "/secret") { + const value = Redacted.value(yield* apiKey); + const masked = value.slice(0, 4) + "****"; + return HttpServerResponse.text(`Secret (masked): ${masked}`); + } + + return HttpServerResponse.text( + "Hello from Cloudflare Secrets Store example!", + ); + }).pipe( + Effect.catchTag("SecretError", (err) => + Effect.succeed( + HttpServerResponse.text(`Failed to read secret: ${err.message}`, { + status: 500, + }), + ), + ), + ), + }; + }).pipe(Effect.provide(Cloudflare.SecretBindingLive)), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/ApiKey.ts b/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/ApiKey.ts new file mode 100644 index 00000000000..007b9abe11a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/ApiKey.ts @@ -0,0 +1,20 @@ +import { Random } from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { Store } from "./Store.ts"; + +/** + * An API key secret in the Secrets Store. + * + * Uses `Random` to generate a stable random value that persists + * across deploys (only regenerated if the resource is replaced). + */ +export const ApiKey = Effect.gen(function* () { + const store = yield* Store; + const secret = yield* Random("ApiKeyValue"); + + return yield* Cloudflare.Secret("ApiKey", { + store, + value: secret.text, + }); +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/Store.ts b/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/Store.ts new file mode 100644 index 00000000000..54158bd44b8 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-secrets-store/src/Store.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const Store = Cloudflare.SecretsStore("SecretStore"); diff --git a/.repos/alchemy-effect/examples/cloudflare-secrets-store/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-secrets-store/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-secrets-store/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/.gitignore b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/.gitignore new file mode 100644 index 00000000000..751513ce1b7 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/.gitignore @@ -0,0 +1,28 @@ +dist +.wrangler +.output +.vercel +.netlify +.vinxi +app.config.timestamp_*.js + +# Environment +.env +.env*.local + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# System Files +.DS_Store +Thumbs.db diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/README.md b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/README.md new file mode 100644 index 00000000000..b4ba3d38ff2 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/README.md @@ -0,0 +1,43 @@ +## Usage + +Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`. + +This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template. + +```bash +$ npm install # or pnpm install or yarn install +``` + +## Exploring the template + +This template's goal is to showcase the routing features of Solid. +It also showcase how the router and Suspense work together to parallelize data fetching tied to a route via the `.data.ts` pattern. + +You can learn more about it on the [`@solidjs/router` repository](https://github.com/solidjs/solid-router) + +### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) + +## Available Scripts + +In the project directory, you can run: + +### `npm run dev` or `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+ +### `npm run build` + +Builds the app for production to the `dist` folder.
+It correctly bundles Solid in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +## Deployment + +You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) + +## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli) diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/alchemy.run.ts new file mode 100644 index 00000000000..7eb402e6615 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/alchemy.run.ts @@ -0,0 +1,27 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export default Alchemy.Stack( + "CloudflareSolidJSSSRExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* Cloudflare.Vite("SolidJSSrr", { + compatibility: { + flags: ["nodejs_compat"], + }, + assets: { + config: { + runWorkerFirst: true, + }, + }, + }); + + return { + url: worker.url, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/index.html b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/index.html new file mode 100644 index 00000000000..f7c1723533a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/index.html @@ -0,0 +1,14 @@ + + + + + + + Solid App + + + +
+ + + diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/package.json b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/package.json new file mode 100644 index 00000000000..3b5e0b90b38 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/package.json @@ -0,0 +1,33 @@ +{ + "name": "cloudflare-solid-ssr", + "version": "0.0.0", + "description": "", + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "preview": "vite preview", + "deploy": "alchemy deploy", + "destroy": "alchemy destroy" + }, + "license": "MIT", + "devDependencies": { + "@tailwindcss/postcss": "^4.1.13", + "postcss": "^8.4.49", + "solid-devtools": "^0.34.3", + "tailwindcss": "^4.1.13", + "vite": "^7.1.4", + "alchemy": "workspace:*", + "effect": "catalog:", + "vite-plugin-solid": "^2.11.8" + }, + "dependencies": { + "@solidjs/router": "^0.15.1", + "solid-js": "^1.9.5" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-solidjs-ssr" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/app.tsx b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/app.tsx new file mode 100644 index 00000000000..1fe135deefd --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/app.tsx @@ -0,0 +1,46 @@ +import { Suspense, type Component } from "solid-js"; +import { A, useLocation } from "@solidjs/router"; + +const App: Component<{ children: Element }> = (props) => { + const location = useLocation(); + + return ( + <> + + +
+ {props.children} +
+ + ); +}; + +export default App; diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/entry-client.tsx b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/entry-client.tsx new file mode 100644 index 00000000000..81a0454467e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/entry-client.tsx @@ -0,0 +1,22 @@ +/* @refresh reload */ +import "solid-devtools"; +import "./index.css"; + +import { hydrate, render } from "solid-js/web"; + +import App from "./app"; +import { Router } from "@solidjs/router"; +import { routes } from "./routes"; + +const root = document.getElementById("root")!; + +const app = () => ( + {props.children}}>{routes} +); + +// Use hydrate when server-rendered content is present, otherwise render (dev mode) +if (root.children.length > 0) { + hydrate(app, root); +} else { + render(app, root); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/entry-server.tsx b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/entry-server.tsx new file mode 100644 index 00000000000..58e9d733bf2 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/entry-server.tsx @@ -0,0 +1,72 @@ +import { renderToStringAsync, generateHydrationScript } from "solid-js/web"; +import { StaticRouter } from "@solidjs/router"; +import App from "./app"; +import { routes } from "./routes"; + +const FALLBACK_HTML = ` + + + + +Solid App + + + +
+ +`; + +async function getTemplate( + env: { ASSETS: Fetcher }, + origin: string, +): Promise { + try { + const res = await env.ASSETS.fetch(new Request(origin + "/index.html")); + if (res.ok) { + const text = await res.text(); + if (text.length > 0) return text; + } + } catch {} + return FALLBACK_HTML; +} + +export default { + async fetch(request: Request, env: { ASSETS: Fetcher }): Promise { + try { + const url = new URL(request.url); + const pathname = url.pathname; + + // Serve static assets via the assets binding + if (pathname.startsWith("/assets/") || pathname.endsWith(".ico")) { + return env.ASSETS.fetch(request); + } + + // Get the HTML template (from assets if available, fallback to bare HTML) + const template = await getTemplate(env, url.origin); + + // Render the SolidJS app to HTML on the server + const appHtml = await renderToStringAsync(() => ( + {props.children}} + > + {routes} + + )); + + // Inject the server-rendered HTML and hydration script + const html = template + .replace("", generateHydrationScript()) + .replace("", appHtml); + + return new Response(html, { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } catch (e: any) { + return new Response(`SSR Error: ${e.message}\n\n${e.stack}`, { + status: 500, + headers: { "Content-Type": "text/plain" }, + }); + } + }, +}; diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/errors/404.tsx b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/errors/404.tsx new file mode 100644 index 00000000000..56e5ad5e3be --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/errors/404.tsx @@ -0,0 +1,8 @@ +export default function NotFound() { + return ( +
+

404: Not Found

+

It's gone 😞

+
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/index.css b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/index.css new file mode 100644 index 00000000000..e654b3ec6b2 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/index.css @@ -0,0 +1,19 @@ +@import "tailwindcss"; + +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/about.data.ts b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/about.data.ts new file mode 100644 index 00000000000..4650d38c0c7 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/about.data.ts @@ -0,0 +1,18 @@ +import { createAsync, query } from "@solidjs/router"; + +function wait(ms: number, data: T): Promise { + return new Promise((resolve) => setTimeout(resolve, ms, data)); +} + +function random(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +const getName = query(() => wait(random(500, 1000), "Solid"), "aboutName"); + +const AboutData = () => { + return createAsync(() => getName()); +}; + +export default AboutData; +export type AboutDataType = typeof AboutData; diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/about.tsx b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/about.tsx new file mode 100644 index 00000000000..2619a31f48c --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/about.tsx @@ -0,0 +1,25 @@ +import { createEffect, Suspense } from "solid-js"; +import AboutData from "./about.data"; + +export default function About() { + const name = AboutData(); + + createEffect(() => { + console.log(name()); + }); + + return ( +
+

About

+ +

A page all about this website.

+ +

+ We love + ...}> +  {name()} + +

+
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/home.tsx b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/home.tsx new file mode 100644 index 00000000000..c773d38b0c0 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/pages/home.tsx @@ -0,0 +1,32 @@ +import { createSignal } from "solid-js"; + +export default function Home() { + const [count, setCount] = createSignal(0); + + return ( +
+

Home

+

This is the home page.

+ +
+ + + Count: {count()} + + +
+
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/routes.ts b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/routes.ts new file mode 100644 index 00000000000..a1b05b52e57 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/src/routes.ts @@ -0,0 +1,21 @@ +import { lazy } from "solid-js"; +import type { RouteDefinition } from "@solidjs/router"; + +import Home from "./pages/home"; +import AboutData from "./pages/about.data"; + +export const routes: RouteDefinition[] = [ + { + path: "/", + component: Home, + }, + { + path: "/about", + component: lazy(() => import("./pages/about")), + data: AboutData, + }, + { + path: "**", + component: lazy(() => import("./errors/404")), + }, +]; diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/tsconfig.json new file mode 100644 index 00000000000..58cbbc56bfc --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + // General + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + // Modules + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + // Type Checking & Safety + "strict": true, + "types": ["vite/client"] + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/vite.config.ts b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/vite.config.ts new file mode 100644 index 00000000000..81b6252077a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidjs-ssr/vite.config.ts @@ -0,0 +1,20 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vite"; +import solidPlugin from "vite-plugin-solid"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + plugins: [solidPlugin({ ssr: true })], + environments: { + ssr: { + build: { + emptyOutDir: false, + rolldownOptions: { + input: resolve(__dirname, "src/entry-server.tsx"), + }, + }, + }, + }, +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/.gitignore b/.repos/alchemy-effect/examples/cloudflare-solidstart/.gitignore new file mode 100644 index 00000000000..751513ce1b7 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/.gitignore @@ -0,0 +1,28 @@ +dist +.wrangler +.output +.vercel +.netlify +.vinxi +app.config.timestamp_*.js + +# Environment +.env +.env*.local + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# System Files +.DS_Store +Thumbs.db diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-solidstart/alchemy.run.ts new file mode 100644 index 00000000000..c866573c232 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/alchemy.run.ts @@ -0,0 +1,22 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export default Alchemy.Stack( + "CloudflareSolidStartExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* Cloudflare.Vite("CloudflareSolidStart", { + compatibility: { + flags: ["nodejs_compat"], + }, + }); + + return { + url: worker.url, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/package.json b/.repos/alchemy-effect/examples/cloudflare-solidstart/package.json new file mode 100644 index 00000000000..896935a57e7 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/package.json @@ -0,0 +1,31 @@ +{ + "name": "example-basic", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "vite start", + "preview": "vite preview", + "deploy": "alchemy deploy", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.15.0", + "@solidjs/start": "2.0.0-alpha.2", + "solid-js": "^1.9.5", + "alchemy": "workspace:*", + "effect": "catalog:", + "vite": "^7.0.0", + "@solidjs/vite-plugin-nitro-2": "^0.1.0" + }, + "engines": { + "node": ">=22" + }, + "devDependencies": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-solidstart" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/public/favicon.ico b/.repos/alchemy-effect/examples/cloudflare-solidstart/public/favicon.ico new file mode 100644 index 00000000000..fb282da0719 Binary files /dev/null and b/.repos/alchemy-effect/examples/cloudflare-solidstart/public/favicon.ico differ diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/app.css b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/app.css new file mode 100644 index 00000000000..0bd39a2a149 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/app.css @@ -0,0 +1,41 @@ +body { + font-family: + Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", + sans-serif; +} + +a { + margin-right: 1rem; +} + +main { + text-align: center; + padding: 1em; + margin: 0 auto; +} + +h1 { + color: #335d92; + text-transform: uppercase; + font-size: 4rem; + font-weight: 100; + line-height: 1.1; + margin: 4rem auto; + max-width: 14rem; +} + +p { + max-width: 14rem; + margin: 2rem auto; + line-height: 1.35; +} + +@media (min-width: 480px) { + h1 { + max-width: none; + } + + p { + max-width: none; + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/app.tsx b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/app.tsx new file mode 100644 index 00000000000..21381656c95 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/app.tsx @@ -0,0 +1,22 @@ +import { MetaProvider, Title } from "@solidjs/meta"; +import { Router } from "@solidjs/router"; +import { FileRoutes } from "@solidjs/start/router"; +import { Suspense } from "solid-js"; +import "./app.css"; + +export default function App() { + return ( + ( + + SolidStart - Basic + Index + About + {props.children} + + )} + > + + + ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/components/Counter.css b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/components/Counter.css new file mode 100644 index 00000000000..308965720fc --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/components/Counter.css @@ -0,0 +1,21 @@ +.increment { + font-family: inherit; + font-size: inherit; + padding: 1em 2em; + color: #335d92; + background-color: rgba(68, 107, 158, 0.1); + border-radius: 2em; + border: 2px solid rgba(68, 107, 158, 0); + outline: none; + width: 200px; + font-variant-numeric: tabular-nums; + cursor: pointer; +} + +.increment:focus { + border: 2px solid #335d92; +} + +.increment:active { + background-color: rgba(68, 107, 158, 0.2); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/components/Counter.tsx b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/components/Counter.tsx new file mode 100644 index 00000000000..954d5563ee5 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/components/Counter.tsx @@ -0,0 +1,15 @@ +import { createSignal } from "solid-js"; +import "./Counter.css"; + +export default function Counter() { + const [count, setCount] = createSignal(0); + return ( + + ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/entry-client.tsx b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/entry-client.tsx new file mode 100644 index 00000000000..0ca4e3c3000 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/entry-client.tsx @@ -0,0 +1,4 @@ +// @refresh reload +import { mount, StartClient } from "@solidjs/start/client"; + +mount(() => , document.getElementById("app")!); diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/entry-server.tsx b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/entry-server.tsx new file mode 100644 index 00000000000..401eff83fdf --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { createHandler, StartServer } from "@solidjs/start/server"; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/[...404].tsx b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/[...404].tsx new file mode 100644 index 00000000000..4ea71ec7fee --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/[...404].tsx @@ -0,0 +1,19 @@ +import { Title } from "@solidjs/meta"; +import { HttpStatusCode } from "@solidjs/start"; + +export default function NotFound() { + return ( +
+ Not Found + +

Page Not Found

+

+ Visit{" "} + + start.solidjs.com + {" "} + to learn how to build SolidStart apps. +

+
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/about.tsx b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/about.tsx new file mode 100644 index 00000000000..c1c2dcf5ad0 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/about.tsx @@ -0,0 +1,10 @@ +import { Title } from "@solidjs/meta"; + +export default function About() { + return ( +
+ About +

About

+
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/index.tsx b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/index.tsx new file mode 100644 index 00000000000..5d557d819fb --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/src/routes/index.tsx @@ -0,0 +1,19 @@ +import { Title } from "@solidjs/meta"; +import Counter from "~/components/Counter"; + +export default function Home() { + return ( +
+ Hello World +

Hello world!

+ +

+ Visit{" "} + + start.solidjs.com + {" "} + to learn how to build SolidStart apps. +

+
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-solidstart/tsconfig.json new file mode 100644 index 00000000000..7692957ff26 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vite/client"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-solidstart/vite.config.ts b/.repos/alchemy-effect/examples/cloudflare-solidstart/vite.config.ts new file mode 100644 index 00000000000..d0128701e17 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-solidstart/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; + +import { solidStart } from "@solidjs/start/config"; + +export default defineConfig({ + plugins: [solidStart()], +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-static-site/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-static-site/alchemy.run.ts new file mode 100644 index 00000000000..6ce07133d56 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-static-site/alchemy.run.ts @@ -0,0 +1,18 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export default Alchemy.Stack( + "CloudflareVite", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* Cloudflare.Vite("Website"); + + return { + url: worker.url, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-static-site/index.html b/.repos/alchemy-effect/examples/cloudflare-static-site/index.html new file mode 100644 index 00000000000..065aecab3f3 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-static-site/index.html @@ -0,0 +1,68 @@ + + + + + + Alchemy + Vite + Cloudflare + + + +
+

Alchemy Effect

+

This Vite app was built and deployed to Cloudflare Workers

+ Deployed with Alchemy +
+
+ + + diff --git a/.repos/alchemy-effect/examples/cloudflare-static-site/package.json b/.repos/alchemy-effect/examples/cloudflare-static-site/package.json new file mode 100644 index 00000000000..d6a83f3307d --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-static-site/package.json @@ -0,0 +1,30 @@ +{ + "name": "cloudflare-vite-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-static-site" + }, + "type": "module", + "scripts": { + "build": "vite build", + "dev:vite": "vite", + "preview": "vite preview", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy" + }, + "dependencies": { + "@distilled.cloud/cloudflare": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "vite": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-static-site/src/main.ts b/.repos/alchemy-effect/examples/cloudflare-static-site/src/main.ts new file mode 100644 index 00000000000..dd42d633bad --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-static-site/src/main.ts @@ -0,0 +1,5 @@ +const app = document.getElementById("app"); +if (app) { + const now = new Date(); + app.innerHTML = `Built at: ${now.toISOString()}`; +} diff --git a/.repos/alchemy-effect/examples/cloudflare-static-site/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-static-site/tsconfig.json new file mode 100644 index 00000000000..586e7d22291 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-static-site/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/.gitignore b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/.gitignore new file mode 100644 index 00000000000..457ac857ecb --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/.gitignore @@ -0,0 +1,6 @@ +node_modules +.tanstack +.nitro +.output +.alchemy +dist diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/alchemy.run.ts new file mode 100644 index 00000000000..259648b1863 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/alchemy.run.ts @@ -0,0 +1,23 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export const Website = Cloudflare.Vite("Website", { + compatibility: { + flags: ["nodejs_compat"], + }, +}); + +export default Alchemy.Stack( + "CloudflareTanstackStartSolidExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const website = yield* Website; + return { + websiteUrl: website.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/package.json b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/package.json new file mode 100644 index 00000000000..ab44b5da119 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/package.json @@ -0,0 +1,34 @@ +{ + "name": "cloudflare-tanstack-start-solid-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-tanstack-start-solid" + }, + "type": "module", + "scripts": { + "dev": "alchemy dev", + "build": "vite build", + "preview": "vite preview", + "deploy": "alchemy deploy", + "destroy": "alchemy destroy", + "test": "bun test" + }, + "dependencies": { + "@tanstack/solid-router": "^1.170.4", + "@tanstack/solid-start": "^1.168.6", + "alchemy": "workspace:*", + "effect": "catalog:", + "solid-js": "^1.9.12" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "@types/bun": "latest", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plugin-solid": "^2.11.12" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routeTree.gen.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routeTree.gen.ts new file mode 100644 index 00000000000..0a8322c6860 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from "./routes/__root"; +import { Route as IndexRouteImport } from "./routes/index"; + +const IndexRoute = IndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => rootRouteImport, +} as any); + +export interface FileRoutesByFullPath { + "/": typeof IndexRoute; +} +export interface FileRoutesByTo { + "/": typeof IndexRoute; +} +export interface FileRoutesById { + __root__: typeof rootRouteImport; + "/": typeof IndexRoute; +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath; + fullPaths: "/"; + fileRoutesByTo: FileRoutesByTo; + to: "/"; + id: "__root__" | "/"; + fileRoutesById: FileRoutesById; +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute; +} + +declare module "@tanstack/solid-router" { + interface FileRoutesByPath { + "/": { + id: "/"; + path: "/"; + fullPath: "/"; + preLoaderRoute: typeof IndexRouteImport; + parentRoute: typeof rootRouteImport; + }; + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +}; +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes(); + +import type { getRouter } from "./router.tsx"; +import type { createStart } from "@tanstack/solid-start"; +declare module "@tanstack/solid-start" { + interface Register { + ssr: true; + router: Awaited>; + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/router.tsx b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/router.tsx new file mode 100644 index 00000000000..8b441765594 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/router.tsx @@ -0,0 +1,15 @@ +import { createRouter } from "@tanstack/solid-router"; +import { routeTree } from "./routeTree.gen.ts"; + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }); +} + +declare module "@tanstack/solid-router" { + interface Register { + router: ReturnType; + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routes/__root.tsx b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routes/__root.tsx new file mode 100644 index 00000000000..1cf905b4e15 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routes/__root.tsx @@ -0,0 +1,40 @@ +/// +import { HeadContent, Scripts, createRootRoute } from "@tanstack/solid-router"; +import type * as Solid from "solid-js"; +import { HydrationScript } from "solid-js/web"; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf-8" }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { title: "TanStack Start Solid on Cloudflare" }, + ], + }), + shellComponent: RootDocument, +}); + +function RootDocument(props: Readonly<{ children: Solid.JSX.Element }>) { + return ( + + + + + + + {props.children} + + + + ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routes/index.tsx b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routes/index.tsx new file mode 100644 index 00000000000..a5f998938f9 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/src/routes/index.tsx @@ -0,0 +1,44 @@ +import { createFileRoute } from "@tanstack/solid-router"; +import { createServerFn } from "@tanstack/solid-start"; +import { createSignal } from "solid-js"; + +export const getServerGreeting = createServerFn({ + method: "GET", +}).handler(() => ({ + message: "Hello from TanStack Start Solid on Cloudflare.Vite.", +})); + +export const Route = createFileRoute("/")({ + loader: () => getServerGreeting(), + component: Home, +}); + +function Home() { + const initial = Route.useLoaderData(); + const [message, setMessage] = createSignal(initial().message); + + return ( +
+

TanStack Start Solid

+

+ This app is served by Cloudflare.Vite. +

+

{message()}

+ +
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/test/integ.test.ts new file mode 100644 index 00000000000..f2e153527e4 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/test/integ.test.ts @@ -0,0 +1,46 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import Stack from "../alchemy.run.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), + stage: "test", +}); + +const stack = beforeAll(deploy(Stack).pipe(Effect.tap(Console.log))); +afterAll( + Effect.gen(function* () { + if (!process.env.NO_DESTROY) { + yield* destroy(Stack); + } + }), +); + +const coldStartRetry = Effect.retry({ + schedule: Schedule.exponential("500 millis").pipe( + Schedule.both(Schedule.recurs(20)), + ), +}); + +test( + "serves the TanStack Start Solid app shell", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + const client = yield* HttpClient.HttpClient; + + const res = yield* client.get(websiteUrl).pipe(coldStartRetry); + expect(res.status).toBe(200); + const html = yield* res.text; + expect(html).toContain("TanStack Start Solid"); + expect(html).toContain( + "Hello from TanStack Start Solid on Cloudflare.Vite", + ); + }), + { timeout: 180_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/tsconfig.json new file mode 100644 index 00000000000..5b4fe7838b7 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "alchemy.run.ts", + "src/**/*.ts", + "src/**/*.tsx", + "test/**/*.ts", + "vite.config.ts" + ], + "compilerOptions": { + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true, + "types": ["bun", "vite/client", "@cloudflare/workers-types"] + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/vite.config.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/vite.config.ts new file mode 100644 index 00000000000..39b205142c0 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack-start-solid/vite.config.ts @@ -0,0 +1,10 @@ +import { tanstackStart } from "@tanstack/solid-start/plugin/vite"; +import { defineConfig } from "vite"; +import viteSolid from "vite-plugin-solid"; + +export default defineConfig({ + resolve: { + tsconfigPaths: true, + }, + plugins: [tanstackStart(), viteSolid({ ssr: true })], +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/.gitignore b/.repos/alchemy-effect/examples/cloudflare-tanstack/.gitignore new file mode 100644 index 00000000000..3be7d630918 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/.gitignore @@ -0,0 +1,5 @@ +node_modules +.tanstack +.nitro +.output +dist diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack/alchemy.run.ts new file mode 100644 index 00000000000..b1198de3019 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/alchemy.run.ts @@ -0,0 +1,32 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import Backend, { Bucket } from "./src/backend.ts"; + +export const Website = Cloudflare.Vite("Website", { + compatibility: { + flags: ["nodejs_compat"], + }, + env: { + BUCKET: Bucket, + BACKEND: Backend, + }, +}); + +export type WebsiteEnv = Cloudflare.InferEnv; + +export default Alchemy.Stack( + "CloudflareTanstackExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const backend = yield* Backend; + const website = yield* Website; + return { + backendUrl: backend.url.as(), + websiteUrl: website.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/package.json b/.repos/alchemy-effect/examples/cloudflare-tanstack/package.json new file mode 100644 index 00000000000..1f8b9229163 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/package.json @@ -0,0 +1,36 @@ +{ + "name": "cloudflare-tanstack-example", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-tanstack" + }, + "type": "module", + "scripts": { + "dev": "alchemy dev", + "build": "vite build", + "preview": "vite preview", + "deploy": "alchemy deploy", + "destroy": "alchemy destroy", + "test": "bun test" + }, + "dependencies": { + "@tanstack/react-router": "^1.167.4", + "@tanstack/react-start": "^1.166.15", + "alchemy": "workspace:*", + "effect": "catalog:", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "^5.9.3", + "vite": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/src/backend.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/backend.ts new file mode 100644 index 00000000000..238a5971c6f --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/backend.ts @@ -0,0 +1,65 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +export const Bucket = Cloudflare.R2Bucket("Bucket"); + +export default class Backend extends Cloudflare.Worker()( + "Backend", + { + main: import.meta.path, + }, + Effect.gen(function* () { + const bucket = yield* Cloudflare.R2Bucket.bind(Bucket); + + return { + // RPC method — read an object from R2 by key, returning the body as + // text or `null` if the key is missing. Other workers can call this + // directly via the service binding (option 3 in `api.hello.ts`). + hello: Effect.fn("Backend.hello")(function* (key: string) { + const object = yield* bucket.get(key); + if (object === null) return null; + return yield* object.text(); + }), + + // HTTP handler — supports `GET` and `PUT` against R2 by `?key=...`. + // Other workers can call this via `env.BACKEND.fetch(...)` (option 2 + // in `api.hello.ts`). + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const key = new URL(request.url, "http://backend").searchParams.get( + "key", + ); + if (!key) { + return HttpServerResponse.text("Missing 'key' query parameter", { + status: 400, + }); + } + + if (request.method === "GET") { + const object = yield* bucket.get(key); + if (object === null) { + return HttpServerResponse.text("Not found", { status: 404 }); + } + return HttpServerResponse.stream(object.body); + } + + if (request.method === "PUT") { + yield* bucket.put(key, request.stream, { + contentLength: Number(request.headers["content-length"] ?? 0), + }); + return HttpServerResponse.empty({ status: 204 }); + } + + return HttpServerResponse.text("Method not allowed", { status: 405 }); + }).pipe( + Effect.catchTag("R2Error", (error) => + Effect.succeed( + HttpServerResponse.text(error.message, { status: 500 }), + ), + ), + ), + }; + }).pipe(Effect.provide(Cloudflare.R2BucketBindingLive)), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/src/env.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/env.ts new file mode 100644 index 00000000000..3f7f56db6e0 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/env.ts @@ -0,0 +1,10 @@ +import * as cf from "cloudflare:workers"; +import type { WebsiteEnv } from "../alchemy.run.ts"; + +// In development mode with TanStack Start, `import { env } from "cloudflare:workers"` does not work at the top level. +// As a workaround, we use a proxy to access the env object. +export const env = new Proxy({} as WebsiteEnv, { + get(_, prop) { + return cf.env[prop as keyof typeof cf.env]; + }, +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routeTree.gen.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routeTree.gen.ts new file mode 100644 index 00000000000..626c6d302e3 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from "./routes/__root"; +import { Route as IndexRouteImport } from "./routes/index"; +import { Route as ApiHelloRouteImport } from "./routes/api.hello"; + +const IndexRoute = IndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => rootRouteImport, +} as any); +const ApiHelloRoute = ApiHelloRouteImport.update({ + id: "/api/hello", + path: "/api/hello", + getParentRoute: () => rootRouteImport, +} as any); + +export interface FileRoutesByFullPath { + "/": typeof IndexRoute; + "/api/hello": typeof ApiHelloRoute; +} +export interface FileRoutesByTo { + "/": typeof IndexRoute; + "/api/hello": typeof ApiHelloRoute; +} +export interface FileRoutesById { + __root__: typeof rootRouteImport; + "/": typeof IndexRoute; + "/api/hello": typeof ApiHelloRoute; +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath; + fullPaths: "/" | "/api/hello"; + fileRoutesByTo: FileRoutesByTo; + to: "/" | "/api/hello"; + id: "__root__" | "/" | "/api/hello"; + fileRoutesById: FileRoutesById; +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute; + ApiHelloRoute: typeof ApiHelloRoute; +} + +declare module "@tanstack/react-router" { + interface FileRoutesByPath { + "/": { + id: "/"; + path: "/"; + fullPath: "/"; + preLoaderRoute: typeof IndexRouteImport; + parentRoute: typeof rootRouteImport; + }; + "/api/hello": { + id: "/api/hello"; + path: "/api/hello"; + fullPath: "/api/hello"; + preLoaderRoute: typeof ApiHelloRouteImport; + parentRoute: typeof rootRouteImport; + }; + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiHelloRoute: ApiHelloRoute, +}; +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes(); + +import type { getRouter } from "./router.tsx"; +import type { createStart } from "@tanstack/react-start"; +declare module "@tanstack/react-start" { + interface Register { + ssr: true; + router: Awaited>; + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/src/router.tsx b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/router.tsx new file mode 100644 index 00000000000..6f7766f8cb3 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/router.tsx @@ -0,0 +1,15 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/__root.tsx b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/__root.tsx new file mode 100644 index 00000000000..920d199d23e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/__root.tsx @@ -0,0 +1,54 @@ +import type { ReactNode } from "react"; +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from "@tanstack/react-router"; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: "utf-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + title: "TanStack Start", + }, + ], + }), + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + ); +} + +function Document(props: Readonly<{ children: ReactNode }>) { + return ( + + + + + + {props.children} + + + + ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/api.hello.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/api.hello.ts new file mode 100644 index 00000000000..c36773592d2 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/api.hello.ts @@ -0,0 +1,128 @@ +import { createFileRoute } from "@tanstack/react-router"; +import * as Cloudflare from "alchemy/Cloudflare/Bridge"; +import type Backend from "../backend.ts"; +import { env } from "../env.ts"; + +const VIAS = ["binding", "fetch", "rpc"] as const; +type Via = (typeof VIAS)[number]; + +const parseRequest = (request: Request): { via: Via; key: string | null } => { + const url = new URL(request.url); + const raw = url.searchParams.get("via") ?? "binding"; + const via = (VIAS as readonly string[]).includes(raw) + ? (raw as Via) + : "binding"; + return { via, key: url.searchParams.get("key") }; +}; + +// Surface the actual error in BOTH the response body and the worker logs — +// otherwise TanStack Start's outer boundary swallows it and you only ever +// see "HTTPError" in `bun alchemy logs`. +const trace = async (label: string, fn: () => Promise) => { + try { + return await fn(); + } catch (err) { + const message = + err instanceof Error ? (err.stack ?? err.message) : String(err); + console.error(`[api.hello] ${label} failed:`, message); + return new Response(`${label} failed: ${message}`, { status: 500 }); + } +}; + +export const Route = createFileRoute("/api/hello")({ + server: { + handlers: { + // GET /api/hello?key=&via=binding|fetch|rpc + GET: async ({ request }) => { + const { via, key } = parseRequest(request); + if (!key) { + return new Response("Missing 'key' query parameter", { status: 400 }); + } + + switch (via) { + // option 1 — use the async binding directly + case "binding": + return trace("GET option 1 (env.BUCKET.get)", async () => { + const object = await env.BUCKET.get(key); + if (!object) return new Response("Not found", { status: 404 }); + return new Response(object.body); + }); + + // option 2 — bind to your effect worker and call fetch + case "fetch": + return trace("GET option 2 (env.BACKEND.fetch)", async () => { + const res = await env.BACKEND.fetch( + `https://backend/?key=${encodeURIComponent(key)}`, + ); + return new Response(res.body, { + status: res.status, + headers: res.headers, + }); + }); + + // option 3 — bind to your effect worker and call rpc method + case "rpc": + return trace("GET option 3 (backend.hello rpc)", async () => { + // Wrap the raw wire-shape binding into a Promise view that + // throws on Effect.fail and unwraps stream envelopes. + const backend = Cloudflare.toRpcAsync(env.BACKEND); + const value = await backend.hello(key); + if (value === null) + return new Response("Not found", { status: 404 }); + return new Response(value); + }); + } + }, + + // PUT /api/hello?key=&via=binding|fetch + // (option 3 is GET-only — `hello` is a read RPC for demonstration.) + PUT: async ({ request }) => { + const { via, key } = parseRequest(request); + if (!key) { + return new Response("Missing 'key' query parameter", { status: 400 }); + } + if (!request.body) { + return new Response("Missing request body", { status: 400 }); + } + + switch (via) { + // option 1 — use the async binding directly + case "binding": + return trace("PUT option 1 (env.BUCKET.put)", async () => { + await env.BUCKET.put(key, request.body, { + httpMetadata: { + contentType: + request.headers.get("content-type") ?? + "application/octet-stream", + }, + }); + return new Response(null, { status: 204 }); + }); + + // option 2 — bind to your effect worker and call fetch + case "fetch": + return trace("PUT option 2 (env.BACKEND.fetch)", async () => { + const res = await env.BACKEND.fetch( + `https://backend/?key=${encodeURIComponent(key)}`, + { + method: "PUT", + body: request.body, + headers: request.headers, + }, + ); + return new Response(res.body, { + status: res.status, + headers: res.headers, + }); + }); + + // option 3 — RPC `hello` is read-only + case "rpc": + return new Response("PUT is not supported via=rpc", { + status: 400, + }); + } + }, + }, + }, +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/index.tsx b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/index.tsx new file mode 100644 index 00000000000..1ab51f0c148 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/src/routes/index.tsx @@ -0,0 +1,63 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { createServerFn } from "@tanstack/react-start"; +import { useState } from "react"; + +export const getServerTime = createServerFn({ + method: "GET", +}).handler(() => ({ + message: "Hello from a TanStack Start server function.", + time: new Date().toISOString(), +})); + +export const Route = createFileRoute("/")({ + loader: () => getServerTime(), + component: Home, +}); + +function Home() { + const initialData = Route.useLoaderData(); + const [data, setData] = useState(initialData); + + return ( +
+

TanStack Start

+

+ This is the minimal app scaffold in{" "} + examples/cloudflare-tanstack. +

+

{data.message}

+

+ {data.time} +

+ +
+ ); +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack/test/integ.test.ts new file mode 100644 index 00000000000..ffe1a978795 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/test/integ.test.ts @@ -0,0 +1,158 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "../alchemy.run.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), + stage: "test", +}); + +const stack = beforeAll(deploy(Stack).pipe(Effect.tap(Console.log))); +afterAll( + Effect.gen(function* () { + if (!process.env.NO_DESTROY) { + yield* destroy(Stack); + } + }), +); + +const route = (url: string, params: Record) => + `${url}/api/hello?${new URLSearchParams(params).toString()}`; + +// Stable per-option keys so re-runs (e.g. NO_DESTROY=1) overwrite cleanly +// instead of leaving stale objects behind. +const KEYS = { + binding: "integ:via-binding", + fetch: "integ:via-fetch", + rpc: "integ:via-rpc", +}; + +// Cold-start retry — fresh `workers.dev` URLs take a few seconds to start +// answering, so the very first PUT in each test rides this schedule. +const coldStartRetry = Effect.retry({ + schedule: Schedule.exponential("500 millis").pipe( + Schedule.both(Schedule.recurs(20)), + ), +}); + +test( + "deploys and exposes a url", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + expect(websiteUrl).toBeString(); + }), + { timeout: 180_000 }, +); + +test( + "option 1 — direct R2 binding round-trips through PUT and GET", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + const client = yield* HttpClient.HttpClient; + const key = KEYS.binding; + + const put = yield* HttpClient.execute( + HttpClientRequest.put(route(websiteUrl, { key, via: "binding" })).pipe( + HttpClientRequest.bodyText("hello-binding", "text/plain"), + ), + ).pipe(coldStartRetry); + expect(put.status).toBe(204); + + const get = yield* client.get(route(websiteUrl, { key, via: "binding" })); + expect(get.status).toBe(200); + expect(yield* get.text).toBe("hello-binding"); + }), + { timeout: 180_000 }, +); + +test( + "option 2 — service-binding fetch into the Backend worker", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + const client = yield* HttpClient.HttpClient; + const key = KEYS.fetch; + + // Write through option 2's PUT path (Backend's fetch handler stores it + // in R2), then read it back through option 2's GET (also Backend.fetch). + const put = yield* HttpClient.execute( + HttpClientRequest.put(route(websiteUrl, { key, via: "fetch" })).pipe( + HttpClientRequest.bodyText("hello-fetch", "text/plain"), + ), + ).pipe(coldStartRetry); + expect(put.status).toBe(204); + + const get = yield* client.get(route(websiteUrl, { key, via: "fetch" })); + expect(get.status).toBe(200); + expect(yield* get.text).toBe("hello-fetch"); + }), + { timeout: 180_000 }, +); + +test( + "option 3 — service-binding RPC method via toPromiseApi", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + const client = yield* HttpClient.HttpClient; + const key = KEYS.rpc; + + // Seed the bucket via option 1 (direct binding) so the RPC `hello` + // method has something to read. + const seed = yield* HttpClient.execute( + HttpClientRequest.put(route(websiteUrl, { key, via: "binding" })).pipe( + HttpClientRequest.bodyText("hello-rpc", "text/plain"), + ), + ).pipe(coldStartRetry); + expect(seed.status).toBe(204); + + // RPC GET reads through Backend.hello — exercises toPromiseApi. + const get = yield* client.get(route(websiteUrl, { key, via: "rpc" })); + expect(get.status).toBe(200); + expect(yield* get.text).toBe("hello-rpc"); + }), + { timeout: 180_000 }, +); + +test( + "missing `key` returns 400", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + const client = yield* HttpClient.HttpClient; + + const res = yield* client.get(route(websiteUrl, { via: "binding" })); + expect(res.status).toBe(400); + }), +); + +test( + "RPC for a non-existent key returns 404", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + const client = yield* HttpClient.HttpClient; + + const res = yield* client.get( + route(websiteUrl, { key: "integ:does-not-exist", via: "rpc" }), + ); + expect(res.status).toBe(404); + }), +); + +test( + "PUT via=rpc returns 400 (RPC `hello` is read-only)", + Effect.gen(function* () { + const { websiteUrl } = yield* stack; + + const res = yield* HttpClient.execute( + HttpClientRequest.put( + route(websiteUrl, { key: "integ:via-options", via: "rpc" }), + ).pipe(HttpClientRequest.bodyText("nope")), + ); + expect(res.status).toBe(400); + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-tanstack/tsconfig.json new file mode 100644 index 00000000000..910a43e8f47 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "alchemy.run.ts", + "src/**/*.ts", + "src/**/*.tsx", + "test/**/*.ts", + "vite.config.ts" + ], + "compilerOptions": { + "rootDir": ".", + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "jsx": "react-jsx", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "types": ["bun", "vite/client", "@cloudflare/workers-types"] + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-tanstack/vite.config.ts b/.repos/alchemy-effect/examples/cloudflare-tanstack/vite.config.ts new file mode 100644 index 00000000000..d37915e1c6d --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-tanstack/vite.config.ts @@ -0,0 +1,7 @@ +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import viteReact from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [tanstackStart(), viteReact()], +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/.gitignore b/.repos/alchemy-effect/examples/cloudflare-vue/.gitignore new file mode 100644 index 00000000000..cd68f14023e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +.eslintcache + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/README.md b/.repos/alchemy-effect/examples/cloudflare-vue/README.md new file mode 100644 index 00000000000..f5e3b4cd6f1 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/README.md @@ -0,0 +1,42 @@ +# ./ + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Recommended Browser Setup + +- Chromium-based browsers (Chrome, Edge, Brave, etc.): + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) +- Firefox: + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +bun install +``` + +### Compile and Hot-Reload for Development + +```sh +bun dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +bun run build +``` diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-vue/alchemy.run.ts new file mode 100644 index 00000000000..4f390e156c9 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/alchemy.run.ts @@ -0,0 +1,23 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export default Alchemy.Stack( + "CloudflareVueExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* Cloudflare.Vite("Vue", { + compatibility: { + flags: ["nodejs_compat"], + }, + memo: {}, + }); + + return { + url: worker.url, + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/index.html b/.repos/alchemy-effect/examples/cloudflare-vue/index.html new file mode 100644 index 00000000000..9d30802cd87 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/package.json b/.repos/alchemy-effect/examples/cloudflare-vue/package.json new file mode 100644 index 00000000000..2850d00971f --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/package.json @@ -0,0 +1,54 @@ +{ + "name": "cloudflare-vue", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "alchemy dev", + "build": "vite build", + "preview": "vite preview", + "deploy": "alchemy deploy", + "destroy": "alchemy destroy" + }, + "dependencies": { + "vue": "beta", + "vue-router": "^5.0.4" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.1", + "npm-run-all2": "^8.0.4", + "oxfmt": "^0.42.0", + "typescript": "~6.0.0", + "vite": "^8.0.3", + "vite-plugin-vue-devtools": "^8.1.1", + "alchemy": "workspace:*", + "effect": "catalog:", + "vue-tsc": "^3.2.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "overrides": { + "vue": "beta", + "@vue/compiler-core": "beta", + "@vue/compiler-dom": "beta", + "@vue/compiler-sfc": "beta", + "@vue/compiler-ssr": "beta", + "@vue/compiler-vapor": "beta", + "@vue/reactivity": "beta", + "@vue/runtime-core": "beta", + "@vue/runtime-dom": "beta", + "@vue/runtime-vapor": "beta", + "@vue/server-renderer": "beta", + "@vue/shared": "beta", + "@vue/compat": "beta" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-vue" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/public/favicon.ico b/.repos/alchemy-effect/examples/cloudflare-vue/public/favicon.ico new file mode 100644 index 00000000000..df36fcfb725 Binary files /dev/null and b/.repos/alchemy-effect/examples/cloudflare-vue/public/favicon.ico differ diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/App.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/App.vue new file mode 100644 index 00000000000..540829f1f50 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/App.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/base.css b/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/base.css new file mode 100644 index 00000000000..4d16fc24679 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Oxygen, + Ubuntu, + Cantarell, + "Fira Sans", + "Droid Sans", + "Helvetica Neue", + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/logo.svg b/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/logo.svg new file mode 100644 index 00000000000..7565660356e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/main.css b/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/main.css new file mode 100644 index 00000000000..cdbdca294d5 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/assets/main.css @@ -0,0 +1,35 @@ +@import "./base.css"; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/HelloWorld.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/HelloWorld.vue new file mode 100644 index 00000000000..fe7b5bc7fea --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/HelloWorld.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/TheWelcome.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/TheWelcome.vue new file mode 100644 index 00000000000..e936a05b80f --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/TheWelcome.vue @@ -0,0 +1,126 @@ + + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/WelcomeItem.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/WelcomeItem.vue new file mode 100644 index 00000000000..d40a319c0da --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/WelcomeItem.vue @@ -0,0 +1,87 @@ + + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconCommunity.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconCommunity.vue new file mode 100644 index 00000000000..ea8ddefb973 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconCommunity.vue @@ -0,0 +1,12 @@ + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconDocumentation.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconDocumentation.vue new file mode 100644 index 00000000000..63a85340aae --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconDocumentation.vue @@ -0,0 +1,12 @@ + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconEcosystem.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconEcosystem.vue new file mode 100644 index 00000000000..385a2029d7b --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconEcosystem.vue @@ -0,0 +1,12 @@ + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconSupport.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconSupport.vue new file mode 100644 index 00000000000..7db961e4d45 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconSupport.vue @@ -0,0 +1,12 @@ + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconTooling.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconTooling.vue new file mode 100644 index 00000000000..660598d7c76 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/main.ts b/.repos/alchemy-effect/examples/cloudflare-vue/src/main.ts new file mode 100644 index 00000000000..7a9c7a853e3 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/main.ts @@ -0,0 +1,11 @@ +import "./assets/main.css"; + +import { createApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; + +const app = createApp(App); + +app.use(router); + +app.mount("#app"); diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/router/index.ts b/.repos/alchemy-effect/examples/cloudflare-vue/src/router/index.ts new file mode 100644 index 00000000000..35612dc5636 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/router/index.ts @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from "vue-router"; +import HomeView from "../views/HomeView.vue"; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + name: "home", + component: HomeView, + }, + { + path: "/about", + name: "about", + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import("../views/AboutView.vue"), + }, + ], +}); + +export default router; diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/views/AboutView.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/views/AboutView.vue new file mode 100644 index 00000000000..756ad2a1790 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/views/AboutView.vue @@ -0,0 +1,15 @@ + + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/src/views/HomeView.vue b/.repos/alchemy-effect/examples/cloudflare-vue/src/views/HomeView.vue new file mode 100644 index 00000000000..23a53cd1bf3 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/src/views/HomeView.vue @@ -0,0 +1,9 @@ + + + diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.app.json b/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.app.json new file mode 100644 index 00000000000..c0f2d86707e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.app.json @@ -0,0 +1,18 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + // Extra safety for array and object lookups, but may have false positives. + "noUncheckedIndexedAccess": true, + + // Path mapping for cleaner imports. + "paths": { + "@/*": ["./src/*"] + }, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.json new file mode 100644 index 00000000000..66b5e5703e8 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.node.json b/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.node.json new file mode 100644 index 00000000000..c9b2badc953 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/tsconfig.node.json @@ -0,0 +1,27 @@ +// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping. +{ + "extends": "@tsconfig/node24/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + // Most tools use transpilation instead of Node.js's native type-stripping. + // Bundler mode provides a smoother developer experience. + "module": "preserve", + "moduleResolution": "bundler", + + // Include Node.js types and avoid accidentally including other `@types/*` packages. + "types": ["node"], + + // Disable emitting output during `vue-tsc --build`, which is used for type-checking only. + "noEmit": true, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-vue/vite.config.ts b/.repos/alchemy-effect/examples/cloudflare-vue/vite.config.ts new file mode 100644 index 00000000000..74831822399 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-vue/vite.config.ts @@ -0,0 +1,15 @@ +import { fileURLToPath, URL } from "node:url"; + +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import vueDevTools from "vite-plugin-vue-devtools"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue(), vueDevTools()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker-async/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-worker-async/alchemy.run.ts new file mode 100644 index 00000000000..94b0be1ccc8 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker-async/alchemy.run.ts @@ -0,0 +1,70 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import { Config } from "effect"; +import * as Effect from "effect/Effect"; +import type { Counter as CounterClass } from "./src/worker.ts"; + +export const DB = Cloudflare.D1Database("DB"); + +export const Bucket = Cloudflare.R2Bucket("Bucket"); + +// Queue producer + consumer wiring (both sides exercised by the same worker). +// The Worker sends a message via `env.QUEUE.send(...)` from POST /queue/send, +// then receives and persists it via its `queue(batch)` handler — end-to-end +// regression guard for the Queue, QueueBinding, and QueueConsumer resources. +export const Queue = Cloudflare.Queue("Queue"); + +export const Counter = Cloudflare.DurableObjectNamespace( + "Counter", + { + className: "Counter", + }, +); + +export type WorkerEnv = Cloudflare.InferEnv; + +export const Worker = Cloudflare.Worker("Worker", { + main: "./src/worker.ts", + assets: { + directory: "./public", + }, + env: { + // Self-contained default so the example deploys without external secrets; + // the integ test asserts this value round-trips through env.API_KEY. + API_KEY: Config.redacted("SOME_API_KEY").pipe( + Config.withDefault("SOME_API_KEY"), + ), + DB, + Bucket, + Queue, + Counter, + }, +}); + +export default Alchemy.Stack( + "CloudflareWorker", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const queue = yield* Queue; + const worker = yield* Worker; + // create a random resource to test redacted storage + yield* Alchemy.Random("Random"); + + // Register the same worker script as a consumer of Queue. The worker's + // `queue(batch)` handler (see src/worker.ts) receives each message batch. + yield* Cloudflare.QueueConsumer("QueueConsumer", { + queueId: queue.queueId, + scriptName: worker.workerName, + settings: { + batchSize: 10, + maxRetries: 3, + maxWaitTimeMs: 5000, + }, + }); + + return worker.url; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker-async/package.json b/.repos/alchemy-effect/examples/cloudflare-worker-async/package.json new file mode 100644 index 00000000000..545fbc02980 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker-async/package.json @@ -0,0 +1,29 @@ +{ + "name": "cloudflare-worker-async", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-worker-async" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + }, + "dependencies": { + "@alchemy.run/better-auth": "workspace:*", + "@cloudflare/workers-types": "catalog:", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "better-auth": "catalog:", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-worker-async/public/file.json b/.repos/alchemy-effect/examples/cloudflare-worker-async/public/file.json new file mode 100644 index 00000000000..77f77415ac0 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker-async/public/file.json @@ -0,0 +1,3 @@ +{ + "message": "Hello, World!" +} diff --git a/.repos/alchemy-effect/examples/cloudflare-worker-async/src/rpc-worker.ts b/.repos/alchemy-effect/examples/cloudflare-worker-async/src/rpc-worker.ts new file mode 100644 index 00000000000..77da1bb4f0c --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker-async/src/rpc-worker.ts @@ -0,0 +1,67 @@ +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import * as Rpc from "effect/unstable/rpc/Rpc"; +import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; +import * as RpcSchema from "effect/unstable/rpc/RpcSchema"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; + +export const API = RpcGroup.make( + Rpc.make("Ping", { + payload: Schema.Void, + success: Schema.String, + }), + Rpc.make("Stream", { + payload: { upto: Schema.Number }, + success: RpcSchema.Stream(Schema.Number, Schema.Never), + }), +); + +const httpEffect = RpcServer.toHttpEffect(API).pipe( + Effect.provide( + Layer.mergeAll( + API.toLayer({ + Ping: () => Effect.succeed("pong"), + Stream: ({ upto }) => + Stream.fromIterable(Array.from({ length: upto }, (_, i) => i)), + }), + RpcSerialization.layerNdjson, + ), + ), +); + +export default { + fetch(request: Request) { + return httpEffect.pipe( + Effect.flatMap((eff) => eff), + Effect.provide([ + Layer.succeed( + HttpServerRequest.HttpServerRequest, + HttpServerRequest.fromWeb(request as any).modify({ + remoteAddress: Option.fromUndefinedOr( + request.headers.get("cf-connecting-ip") ?? undefined, + ), + }), + ), + FetchHttpClient.layer, + ]), + Effect.flatMap((response) => + Effect.context().pipe( + Effect.map((context) => + HttpServerResponse.toWeb(response as any, { + context, + }), + ), + ), + ), + Effect.scoped, + Effect.runPromise, + ); + }, +}; diff --git a/.repos/alchemy-effect/examples/cloudflare-worker-async/src/worker.ts b/.repos/alchemy-effect/examples/cloudflare-worker-async/src/worker.ts new file mode 100644 index 00000000000..2b807841706 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker-async/src/worker.ts @@ -0,0 +1,88 @@ +import { DurableObject } from "cloudflare:workers"; +import type { WorkerEnv } from "../alchemy.run.ts"; + +/** + * Type of messages we send on the queue — kept minimal so the example stays + * self-contained. Both producer (`env.Queue.send(...)`) and consumer (the + * `queue()` handler below) use this shape. + */ +interface QueueMessage { + id: string; + text: string; + sentAt: number; +} + +export default { + async fetch(request: Request, env: WorkerEnv) { + const url = new URL(request.url); + const path = url.pathname; + + // Echo back env.API_KEY so the integ test can verify the env round-trip. + if (request.method === "GET" && path === "/api-key") { + return new Response(env.API_KEY, { + headers: { "content-type": "text/plain" }, + }); + } + + // Queue producer — POST /queue/send?text=... + // + // Exercises Cloudflare.QueueBinding by calling `env.Queue.send(...)`. + // The message is persisted by the consumer handler into R2 at /queue/ + // so the integ test can read it back and assert the full round-trip. + if (request.method === "POST" && path === "/queue/send") { + const text = url.searchParams.get("text") ?? "hello queue"; + const msg: QueueMessage = { + id: crypto.randomUUID(), + text, + sentAt: Date.now(), + }; + await env.Queue.send(msg); + return new Response(JSON.stringify({ sent: msg }), { + status: 202, + headers: { "content-type": "application/json" }, + }); + } + + if (request.method === "GET") { + return new Response((await env.Bucket.get(path))?.body ?? null); + } else if (request.method === "PUT") { + const object = (await env.Bucket.put(path, request.body))!; + return new Response( + JSON.stringify({ + key: object.key, + size: object.size, + }), + { status: 201 }, + ); + } else if (request.method === "POST") { + const counter = env.Counter.getByName("counter"); + return new Response(JSON.stringify({ count: await counter.increment() })); + } + return env.ASSETS.fetch(request); + }, + + /** + * Queue consumer handler — invoked by Cloudflare when messages accumulate + * on the Queue registered as a QueueConsumer for this worker. + * + * Persists each message body into R2 under `/queue/` so the integ test + * (or a manual `GET /queue/`) can verify the round-trip succeeded. + * `msg.ack()` marks the message as consumed so it isn't redelivered. + */ + async queue(batch: MessageBatch, env: WorkerEnv) { + for (const msg of batch.messages) { + await env.Bucket.put(`/queue/${msg.body.id}`, JSON.stringify(msg.body), { + httpMetadata: { contentType: "application/json" }, + }); + msg.ack(); + } + }, +}; + +export class Counter extends DurableObject { + private counter = 0; + + async increment() { + return ++this.counter; + } +} diff --git a/.repos/alchemy-effect/examples/cloudflare-worker-async/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-worker-async/test/integ.test.ts new file mode 100644 index 00000000000..19928155f22 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker-async/test/integ.test.ts @@ -0,0 +1,86 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Effect from "effect/Effect"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "../alchemy.run.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), +}); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "deploys and exposes a url", + Effect.gen(function* () { + const out = (yield* stack) as unknown; + const url = typeof out === "string" ? out : (out as { url: string }).url; + expect(url).toBeString(); + }), +); + +test( + "echoes env.API_KEY", + Effect.gen(function* () { + const out = (yield* stack) as unknown; + const url = typeof out === "string" ? out : (out as { url: string }).url; + + const response = yield* HttpClient.get(`${url}/api-key`); + expect(response.status).toBe(200); + const body = yield* response.text; + expect(body).toBe("SOME_API_KEY"); + }), +); + +/** + * Native (async) queue handler round-trip. The async worker exports + * a plain `queue(batch, env)` handler that writes each message body + * to R2 at `/queue/`. POST /queue/send enqueues a message; + * GET / reads from R2, so we read /queue/ back. + * + * Pairs with the cloudflare-worker example, which exercises the + * Effect-style `Cloudflare.messages(Queue).subscribe(...)` path + * against the same producer/consumer round-trip. + */ +test( + "native queue() handler round-trip", + Effect.gen(function* () { + const out = (yield* stack) as unknown; + const url = typeof out === "string" ? out : (out as { url: string }).url; + const text = `hello-${Date.now()}`; + + const sendResponse = yield* HttpClient.execute( + HttpClientRequest.post( + `${url}/queue/send?text=${encodeURIComponent(text)}`, + ), + ); + expect(sendResponse.status).toBe(202); + const { sent } = (yield* sendResponse.json) as { + sent: { id: string; text: string; sentAt: number }; + }; + expect(sent.id).toBeTypeOf("string"); + + const deadline = Date.now() + 60_000; + let consumed: { id: string; text: string; sentAt: number } | undefined; + while (Date.now() < deadline) { + const resultResponse = yield* HttpClient.get(`${url}/queue/${sent.id}`); + if (resultResponse.status === 200) { + const body = yield* resultResponse.text; + if (body) { + consumed = JSON.parse(body); + break; + } + } + yield* Effect.sleep("2 seconds"); + } + + expect(consumed).toBeDefined(); + expect(consumed!.id).toBe(sent.id); + expect(consumed!.text).toBe(text); + }), + { timeout: 120_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker-async/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-worker-async/tsconfig.json new file mode 100644 index 00000000000..d69b08a7992 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker-async/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts", "test/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "types": ["bun", "@cloudflare/workers-types"] + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + }, + { + "path": "../../packages/better-auth/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/alchemy.run.ts b/.repos/alchemy-effect/examples/cloudflare-worker/alchemy.run.ts new file mode 100644 index 00000000000..de2eb8bcecd --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/alchemy.run.ts @@ -0,0 +1,56 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +import { Gateway } from "./src/AiGateway.ts"; +import Api from "./src/Api.ts"; +import { Bucket } from "./src/Bucket.ts"; +import SecondaryApiLive, { SecondaryApi } from "./src/SecondaryApi.ts"; +import WorkerTagLive, { WorkerTag } from "./src/WorkerTag.ts"; + +// Demo Action — runs at deploy time when its input (the resolved deployed +// URL) changes. Logs the new URL and returns a tiny manifest used as the +// stack output. Re-deploys with no changes skip the body. +const AnnounceDeploy = Alchemy.Action( + "AnnounceDeploy", + (input: { url: string; bucket: string }) => + Effect.gen(function* () { + yield* Effect.log(`Deployed ${input.url} (bucket: ${input.bucket})`); + return { deployedAt: new Date().toISOString(), url: input.url }; + }), +); + +export default Alchemy.Stack( + "CloudflareWorkerExample", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const api = yield* Api; + const bucket = yield* Bucket; + const gateway = yield* Gateway; + const workerTag = yield* WorkerTag; + // Two Workers binding the same Agent DO triggers the regression where + // a single Container DO namespace appears in multiple bindings on the + // Sandbox ContainerApplication. See SecondaryApi.ts for details. + const secondaryApi = yield* SecondaryApi; + // The Queue consumer is wired automatically by + // `Cloudflare.messages(Queue).subscribe(...)` inside src/Api.ts — + // no explicit `Cloudflare.QueueConsumer(...)` is needed here. + + const announcement = yield* AnnounceDeploy({ + url: api.url.as(), + bucket: bucket.bucketName, + }); + + return { + url: api.url.as(), + bucket: bucket.bucketName, + gatewayId: gateway.gatewayId, + workerTagUrl: workerTag.url.as(), + secondaryApiUrl: secondaryApi.url.as(), + deployedAt: announcement.deployedAt, + }; + }).pipe(Effect.provide(WorkerTagLive), Effect.provide(SecondaryApiLive)), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/assets/index.html b/.repos/alchemy-effect/examples/cloudflare-worker/assets/index.html new file mode 100644 index 00000000000..657c6f85362 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/assets/index.html @@ -0,0 +1,1023 @@ + + + + + + Room Chat + + + +
+
+

Room Chat

+
+ + + +
+
+ disconnected +
+

Auth

+
+ + + +
+
+ + +
+
Idle.
+
+
+
+ +
+
+

Scheduled Reminders

+
+ + seconds + + +
+
+ Type /remind <seconds> <message> in chat, or use the form + above. +
+
+
+

Artifacts (Git for Agents)

+
+ + + + + +
+
+ Create a repo, then mint scoped tokens for git clone/push. +
+
+
+

AI Gateway

+
+ + +
+
+ Routes Workers AI calls through Cloudflare AI Gateway for caching, + rate limiting and observability. +
+
+
+

NotifyWorkflow

+ +
+ Idle — messages will appear in the chat above. +
+
+
+ + +
+
+ + + + diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/package.json b/.repos/alchemy-effect/examples/cloudflare-worker/package.json new file mode 100644 index 00000000000..0b13905f0f3 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/package.json @@ -0,0 +1,28 @@ +{ + "name": "cloudflare-worker", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/cloudflare-worker" + }, + "type": "module", + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + }, + "dependencies": { + "@alchemy.run/better-auth": "workspace:*", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "better-auth": "catalog:", + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/Agent.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/Agent.ts new file mode 100644 index 00000000000..c1f2d93c150 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/Agent.ts @@ -0,0 +1,81 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import { Sandbox } from "./Sandbox.ts"; + +export default class Agent extends Cloudflare.DurableObjectNamespace()( + "Agents", + Effect.gen(function* () { + const sandbox = yield* Cloudflare.Container.bind(Sandbox); + + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + const container = yield* Cloudflare.start(sandbox, { + enableInternet: true, + }); + + const sessions = new Map(); + + for (const socket of yield* state.getWebSockets()) { + const session = socket.deserializeAttachment<{ id: string }>(); + if (session) { + sessions.set(session.id, socket); + } + } + + return { + exec: (command: string) => container.exec(command), + hello: () => + Effect.gen(function* () { + const { fetch } = yield* container.getTcpPort(3000); + const response = yield* fetch( + HttpClientRequest.get("http://container/"), + ); + return yield* response.text; + }), + increment: () => + Effect.gen(function* () { + const { fetch } = yield* container.getTcpPort(3000); + const response = yield* fetch( + HttpClientRequest.post("http://container/increment"), + ); + return yield* response.text; + }), + fetch: Effect.gen(function* () { + const [response, socket] = yield* Cloudflare.upgrade(); + const id = "TODO"; + socket.serializeAttachment({ id }); + sessions.set(id, socket); + return response; + }), + webSocketMessage: Effect.fnUntraced(function* ( + socket: Cloudflare.DurableWebSocket, + message: string | Uint8Array, + ) { + const session = socket.deserializeAttachment<{ id: string }>(); + if (!session) return; + const text = + typeof message === "string" + ? message + : new TextDecoder().decode(message); + for (const peer of sessions.values()) { + yield* peer.send(`[${session.id}] ${text}`); + } + }), + webSocketClose: Effect.fnUntraced(function* ( + ws: Cloudflare.DurableWebSocket, + code: number, + reason: string, + _wasClean: boolean, + ) { + const session = ws.deserializeAttachment<{ id: string }>(); + if (session) { + sessions.delete(session.id); + } + yield* ws.close(code, reason); + }), + }; + }); + }), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/AiGateway.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/AiGateway.ts new file mode 100644 index 00000000000..348f0850fa3 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/AiGateway.ts @@ -0,0 +1,6 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const Gateway = Cloudflare.AiGateway("Gateway", { + cacheTtl: 60, + collectLogs: true, +}); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/Api.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/Api.ts new file mode 100644 index 00000000000..b3041797f2c --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/Api.ts @@ -0,0 +1,458 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as HttpBody from "effect/unstable/http/HttpBody"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import Agent from "./Agent.ts"; +import { Gateway } from "./AiGateway.ts"; +import { Bucket } from "./Bucket.ts"; +import { KV } from "./KV.ts"; +import NotifyWorkflow from "./NotifyWorkflow.ts"; +import { Queue } from "./Queue.ts"; +import { Repos } from "./Repos.ts"; +import Room from "./Room.ts"; + +interface QueueMessageBody { + id: string; + text: string; + sentAt: number; +} + +export default class Api extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + observability: { + enabled: true, + }, + assets: "./assets", + build: { + bundleAnalyzer: true, + }, + }, + Effect.gen(function* () { + // const betterAuth = yield* BetterAuth.BetterAuth; + const agents = yield* Agent; + const rooms = yield* Room; + const notifier = yield* NotifyWorkflow; + const loader = yield* Cloudflare.DynamicWorkerLoader("Loader"); + const bucket = yield* Cloudflare.R2Bucket.bind(Bucket); + const kv = yield* Cloudflare.KVNamespace.bind(KV); + const queueResource = yield* Queue; + const queue = yield* Cloudflare.QueueBinding.bind(queueResource); + const repos = yield* Cloudflare.Artifacts.bind(Repos); + const aiGateway = yield* Cloudflare.AiGateway.bind(Gateway); + + // Effect-style queue consumer. Each batch is piped through the + // handler; success ack()s every message in the batch, failure + // retry()s. The persisted JSON at /queue/ on R2 lets the + // integ test verify the producer→consumer round-trip. + yield* Cloudflare.messages(queueResource).subscribe( + (stream) => + Stream.runForEach(stream, (msg) => + bucket + .put(`/queue/${msg.body.id}`, JSON.stringify(msg.body), { + httpMetadata: { contentType: "application/json" }, + }) + .pipe(Effect.asVoid), + ), + ); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + + if (request.url.startsWith("/auth/")) { + // return yield* betterAuth.fetch; + } else if (request.url.startsWith("/kv/")) { + if (request.method === "GET") { + const key = request.url.split("/").pop()!; + return yield* kv.get(key).pipe( + Effect.map((value) => + value + ? HttpServerResponse.text(value) + : HttpServerResponse.empty({ status: 404 }), + ), + Effect.catch(() => + Effect.succeed(HttpServerResponse.empty({ status: 404 })), + ), + ); + } else if (request.method === "POST") { + const key = request.url.split("/").pop()!; + const value = yield* request.text; + return yield* kv.put(key, value).pipe( + Effect.map(() => HttpServerResponse.empty({ status: 200 })), + Effect.catch(() => + Effect.succeed(HttpServerResponse.empty({ status: 500 })), + ), + ); + } + } else if (request.url.startsWith("/object/")) { + if (request.method === "GET") { + return yield* bucket.get(request.url.split("/").pop()!).pipe( + Effect.flatMap((object) => + object === null + ? Effect.succeed( + HttpServerResponse.text("Object not found", { + status: 404, + }), + ) + : object.text().pipe( + Effect.map((text) => + HttpServerResponse.text(text, { + headers: { "content-type": "application/json" }, + }), + ), + ), + ), + Effect.catchTag("R2Error", (error) => + Effect.succeed( + HttpServerResponse.text(error.message, { + status: 500, + statusText: error.message, + }), + ), + ), + ); + } else if (request.method === "POST" || request.method === "PUT") { + // const request = yield* Cloudflare.Request + const key = request.url.split("/").pop()!; + return yield* bucket + .put(key, request.stream, { + contentLength: Number(request.headers["content-length"] ?? 0), + }) + .pipe( + Effect.map(() => HttpServerResponse.empty({ status: 201 })), + Effect.catch((err) => + HttpServerResponse.json( + { + error: err.message, + headers: request.headers, + }, + { status: 500 }, + ), + ), + ); + } else { + return HttpServerResponse.text("Method not allowed", { + status: 405, + }); + } + } else if (request.url === "/sandbox/increment") { + const agent = agents.getByName("sandbox-test"); + const body = yield* agent.increment().pipe(Effect.orDie); + const room = rooms.getByName("default"); + yield* room.broadcast(`[container] ${body}`); + return HttpServerResponse.text(body, { + headers: { "content-type": "application/json" }, + }); + } else if (request.url.startsWith("/sandbox")) { + const agent = agents.getByName("sandbox-test"); + const body = yield* agent.hello().pipe(Effect.orDie); + return HttpServerResponse.text(body); + } else if (request.url.startsWith("/workflow/start/")) { + const roomId = request.url.split("/workflow/start/")[1]; + if (!roomId) { + return yield* HttpServerResponse.json( + { error: "roomId is required" }, + { status: 400 }, + ); + } + const instance = yield* notifier.create({ + roomId, + message: "hello from workflow", + }); + return yield* HttpServerResponse.json({ instanceId: instance.id }); + } else if (request.url.startsWith("/workflow/status/")) { + const instanceId = request.url.split("/workflow/status/")[1]; + if (!instanceId) { + return yield* HttpServerResponse.json( + { error: "instanceId is required" }, + { status: 400 }, + ); + } + const instance = yield* notifier.get(instanceId); + const status = yield* instance.status(); + return yield* HttpServerResponse.json(status); + } else if (request.url.startsWith("/eval")) { + if (request.method === "POST") { + const code = yield* request.text; + const worker = loader.load({ + compatibilityDate: "2026-01-28", + mainModule: "worker.js", + modules: { + "worker.js": ` + export default { + async fetch(request) { + try { + const result = (0, eval)(${"`${await request.text()}`"}); + return new Response(String(result), { status: 200 }); + } catch (e) { + return new Response(e.message, { status: 500 }); + } + } + } + `, + }, + globalOutbound: null, + }); + return yield* worker + .fetch( + HttpClientRequest.post("https://worker/").pipe( + HttpClientRequest.setBody(HttpBody.text(code)), + ), + ) + .pipe( + Effect.map(HttpServerResponse.fromClientResponse), + Effect.orDie, + ); + } + } else if (request.url.startsWith("/connect/")) { + const agentId = request.url.split("/").pop()!; + const agent = agents.getByName(agentId); + const response = yield* agent.fetch(request); + return response; + } else if (request.url.startsWith("/room/")) { + const upgradeHeader = request.headers.upgrade; + const roomId = request.url.split("/").pop()!; + if (!upgradeHeader || upgradeHeader !== "websocket") { + return HttpServerResponse.text( + "Worker expected Upgrade: websocket", + { status: 426 }, + ); + } else if (request.method !== "GET") { + return HttpServerResponse.text("Method not allowed", { + status: 405, + }); + } + const room = rooms.getByName(roomId); + const response = yield* room.fetch(request); + return response; + } + // Cloudflare Artifacts — Git-compatible versioned repos. + // Exercises Cloudflare.ArtifactsConnection by creating a repo, + // looking it up, and minting short-lived clone tokens. + if ( + request.url.startsWith("/repos/create") && + request.method === "POST" + ) { + const text = yield* request.text; + const body = JSON.parse(text || "{}") as { + name?: string; + description?: string; + }; + const name = body.name?.trim(); + if (!name) { + return yield* HttpServerResponse.json( + { error: "name is required" }, + { status: 400 }, + ); + } + return yield* repos + .create(name, { + description: body.description, + setDefaultBranch: "main", + }) + .pipe( + Effect.flatMap((created) => + HttpServerResponse.json({ + id: created.id, + name: created.name, + remote: created.remote, + token: created.token, + tokenExpiresAt: created.tokenExpiresAt, + defaultBranch: created.defaultBranch, + }), + ), + Effect.catchTag("ArtifactsError", (err) => + HttpServerResponse.json( + { error: err.message }, + { status: 409 }, + ), + ), + ); + } + if (request.url.startsWith("/repos/list") && request.method === "GET") { + return yield* repos.list({ limit: 50 }).pipe( + Effect.flatMap((res) => HttpServerResponse.json(res)), + Effect.catchTag("ArtifactsError", (err) => + HttpServerResponse.json({ error: err.message }, { status: 500 }), + ), + ); + } + if (request.url.startsWith("/repos/info") && request.method === "GET") { + const name = new URL(request.url, "http://x").searchParams.get( + "name", + ); + if (!name) { + return yield* HttpServerResponse.json( + { error: "name is required" }, + { status: 400 }, + ); + } + return yield* repos.get(name).pipe( + Effect.flatMap((repo) => + HttpServerResponse.json({ + id: repo.raw.id, + name: repo.raw.name, + description: repo.raw.description, + defaultBranch: repo.raw.defaultBranch, + remote: repo.raw.remote, + createdAt: repo.raw.createdAt, + updatedAt: repo.raw.updatedAt, + lastPushAt: repo.raw.lastPushAt, + readOnly: repo.raw.readOnly, + }), + ), + Effect.catchTag("ArtifactsError", (err) => + HttpServerResponse.json( + { name, error: err.message }, + { status: 404 }, + ), + ), + ); + } + if ( + request.url.startsWith("/repos/token") && + request.method === "POST" + ) { + const text = yield* request.text; + const body = JSON.parse(text || "{}") as { + name?: string; + scope?: "read" | "write"; + ttl?: number; + }; + const name = body.name?.trim(); + if (!name) { + return yield* HttpServerResponse.json( + { error: "name is required" }, + { status: 400 }, + ); + } + return yield* repos.get(name).pipe( + Effect.flatMap((repo) => + repo.createToken(body.scope ?? "read", body.ttl ?? 3600), + ), + Effect.flatMap((token) => + HttpServerResponse.json({ name, ...token }), + ), + Effect.catchTag("ArtifactsError", (err) => + HttpServerResponse.json( + { name, error: err.message }, + { status: 404 }, + ), + ), + ); + } + // Queue producer + consumer smoke test. + // + // POST /queue/send sends a message with a generated id. + // GET /queue/result/:id reads the bucket entry the consumer + // wrote when it processed that message. + // + // Producer side: `Cloudflare.QueueBinding`. Consumer side: + // `Cloudflare.messages(Queue).subscribe(...)` registered in + // the init phase (above), with `QueueEventSourceLive` on the + // worker layer. + // AI Gateway smoke test — POST /ai with { prompt }. + // + // Routes a Workers AI inference call through the gateway resource so + // every request is observable in the Cloudflare AI Gateway UI and + // benefits from caching/rate limiting configured on the resource. + if (request.url.startsWith("/ai") && request.method === "POST") { + const text = yield* request.text; + const body = (() => { + try { + return JSON.parse(text || "{}") as { prompt?: string }; + } catch { + return {} as { prompt?: string }; + } + })(); + const prompt = + body.prompt?.trim() || "Say hello in one short sentence."; + const response = yield* aiGateway.run({ + provider: "workers-ai", + endpoint: "@cf/meta/llama-3.1-8b-instruct", + headers: { "content-type": "application/json" }, + query: { prompt }, + }); + return HttpServerResponse.fromWeb(response); + } + if (request.url === "/queue/send" && request.method === "POST") { + const text = yield* request.text; + const msg: QueueMessageBody = { + id: crypto.randomUUID(), + text, + sentAt: Date.now(), + }; + yield* queue.send(msg).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ sent: msg }, { status: 202 }); + } + if (request.url.startsWith("/queue/result/")) { + const id = request.url.split("/queue/result/")[1]; + if (!id) { + return HttpServerResponse.text("missing id", { status: 400 }); + } + if (request.method === "GET") { + return yield* bucket.get(`/queue/${id}`).pipe( + Effect.flatMap((object) => + object === null + ? Effect.succeed( + HttpServerResponse.text("not yet", { status: 404 }), + ) + : object.text().pipe( + Effect.map((body) => + HttpServerResponse.text(body, { + headers: { "content-type": "application/json" }, + }), + ), + ), + ), + Effect.catchTag("R2Error", (error) => + Effect.succeed( + HttpServerResponse.text(error.message, { status: 500 }), + ), + ), + ); + } + // DELETE — used by the integ test to clear consumed + // entries before stack.destroy(), so R2Bucket delete + // doesn't fail with "bucket not empty". + if (request.method === "DELETE") { + return yield* bucket.delete(`/queue/${id}`).pipe( + Effect.map(() => HttpServerResponse.empty({ status: 204 })), + Effect.catchTag("R2Error", (error) => + Effect.succeed( + HttpServerResponse.text(error.message, { status: 500 }), + ), + ), + ); + } + } + return HttpServerResponse.text("Not Found", { status: 404 }); + }).pipe( + Effect.catch(() => + Effect.succeed( + HttpServerResponse.text("Internal Server Error", { + status: 500, + }), + ), + ), + ), + }; + }).pipe( + Effect.provide( + Layer.mergeAll( + Cloudflare.R2BucketBindingLive, + Cloudflare.KVNamespaceBindingLive, + Cloudflare.QueueBindingLive, + Cloudflare.QueueEventSourceLive, + Cloudflare.ArtifactsBindingLive, + Cloudflare.AiGatewayBindingLive, + ), + ), + ), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/Bucket.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/Bucket.ts new file mode 100644 index 00000000000..23edbf011de --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/Bucket.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const Bucket = Cloudflare.R2Bucket("Bucket"); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/KV.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/KV.ts new file mode 100644 index 00000000000..a423af5f86b --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/KV.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const KV = Cloudflare.KVNamespace("KV"); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/NotifyWorkflow.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/NotifyWorkflow.ts new file mode 100644 index 00000000000..846c3ffc50d --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/NotifyWorkflow.ts @@ -0,0 +1,84 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import { Config } from "effect"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import { KV } from "./KV.ts"; +import Room from "./Room.ts"; + +/** + * Hard-coded value the integ test asserts on to prove the secret was + * bound at plantime and read at runtime through the `Redacted` accessor. + */ +export const WORKFLOW_SECRET_VALUE = Redacted.make("wf-secret-abc123"); + +export default class NotifyWorkflow extends Cloudflare.Workflow()( + "Notifier", + Effect.gen(function* () { + const rooms = yield* Room; + + const secret = yield* Config.redacted("WORKFLOW_SECRET").pipe( + Config.withDefault(WORKFLOW_SECRET_VALUE), + ); + + // Regression guard for https://github.com/alchemy-run/alchemy-effect/pull/71 + // + // The kv binding internally yields `Cloudflare.WorkerEnvironment` — + // before that PR, accessing `WorkerEnvironment` inside a workflow body + // crashed because `provideService(WorkerEnvironment, env)` was applied + // to the outer `Effect.succeed(body)` wrapper (a no-op) instead of + // `body` itself in `Workflow.ts`. Exercising `kv.put` / `kv.get` from + // inside a `task` keeps the integ test catching any future regression. + const kv = yield* Cloudflare.KVNamespace.bind(KV); + + return Effect.fn(function* (input: { roomId: string; message: string }) { + const { roomId, message } = input; + + const stored = yield* Cloudflare.task( + "kv-roundtrip", + Effect.gen(function* () { + const key = `workflow:smoke:${roomId}`; + yield* kv.put(key, message); + const got = yield* kv.get(key); + if (got !== message) { + return yield* Effect.die( + new Error( + `KV roundtrip mismatch: expected "${message}", got "${got ?? "null"}"`, + ), + ); + } + return got; + }).pipe(Effect.orDie), + ); + + // Resolve the bound secret inside the workflow body. The accessor + // returns `Redacted`; unwrap only where the value needs to + // leave the workflow (here, in the broadcast + the returned output + // so the integ test can assert end-to-end propagation). + const secretValue = Redacted.value(secret); + + const processed = yield* Cloudflare.task( + "process", + Effect.succeed({ + text: `Processed: ${stored}`, + secret: secretValue, + ts: Date.now(), + }), + ); + + const room = rooms.getByName(roomId); + yield* Cloudflare.task( + "broadcast", + room.broadcast(`[workflow] ${processed.text} secret=${secretValue}`), + ); + + yield* Cloudflare.sleep("cooldown", "2 seconds"); + + yield* Cloudflare.task( + "finalize", + room.broadcast(`[workflow] complete for ${roomId}`), + ); + + return processed; + }); + }), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/Queue.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/Queue.ts new file mode 100644 index 00000000000..08ad52e247e --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/Queue.ts @@ -0,0 +1,13 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +/** + * Queue resource used by the Api worker as a producer (via + * `Cloudflare.QueueBinding.bind(Queue)`). Exercises the Queue + QueueBinding + * resources end-to-end in the Effect-based example. + * + * The async example (`examples/cloudflare-worker-async`) demonstrates the + * consumer side via a native `queue()` handler on the default export — the + * Effect worker's `Main` type currently only exposes a `fetch` handler, so + * consumer-side wiring on Effect workers is a follow-up. + */ +export const Queue = Cloudflare.Queue("Queue"); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/Repos.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/Repos.ts new file mode 100644 index 00000000000..2ed940cd088 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/Repos.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const Repos = Cloudflare.Artifacts("Repos"); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/Room.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/Room.ts new file mode 100644 index 00000000000..30ef1242a95 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/Room.ts @@ -0,0 +1,94 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +/** + * Ephemeral chat room: broadcasts each text message to every connected client. + * Uses Durable Object storage of WebSocket attachments so sessions survive hibernation. + * + * Also demonstrates scheduled events: send `/remind ` to + * schedule a broadcast that fires after the given delay. + */ +export default class Room extends Cloudflare.DurableObjectNamespace()( + "Rooms", + Effect.gen(function* () { + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + const sessions = new Map(); + + for (const socket of yield* state.getWebSockets()) { + const attachment = socket.deserializeAttachment<{ id: string }>(); + if (attachment) { + sessions.set(attachment.id, socket); + } + } + + const broadcast = (text: string) => + Effect.gen(function* () { + for (const peer of sessions.values()) { + yield* peer.send(text); + } + }); + + return { + fetch: Effect.gen(function* () { + const [response, socket] = yield* Cloudflare.upgrade(); + const id = crypto.randomUUID(); + socket.serializeAttachment({ id }); + sessions.set(id, socket); + return response; + }), + broadcast, + alarm: () => + Effect.gen(function* () { + const fired = yield* Cloudflare.processScheduledEvents; + for (const event of fired) { + const payload = event.payload as { message: string }; + yield* broadcast(`[reminder] ${payload.message}`); + } + }), + webSocketMessage: Effect.fnUntraced(function* ( + socket: Cloudflare.DurableWebSocket, + message: string | ArrayBuffer, + ) { + const attachment = socket.deserializeAttachment<{ id: string }>(); + if (!attachment) return; + const text = + typeof message === "string" + ? message + : new TextDecoder().decode(message); + + const remindMatch = text.match(/^\/remind\s+(\d+)\s+(.+)$/); + if (remindMatch) { + const delaySec = parseInt(remindMatch[1], 10); + const msg = remindMatch[2]; + const id = crypto.randomUUID(); + const runAt = new Date(Date.now() + delaySec * 1000); + yield* Cloudflare.scheduleEvent(id, runAt, { message: msg }); + yield* socket.send( + `[system] Reminder scheduled in ${delaySec}s: "${msg}"`, + ); + return; + } + + const label = attachment.id.slice(0, 8); + for (const peer of sessions.values()) { + yield* peer.send(`[${label}] ${text}`); + } + }), + webSocketClose: Effect.fnUntraced(function* ( + ws: Cloudflare.DurableWebSocket, + code: number, + reason: string, + _wasClean: boolean, + ) { + const attachment = ws.deserializeAttachment<{ id: string }>(); + if (attachment) { + sessions.delete(attachment.id); + } + yield* ws.close(code, reason); + }), + }; + }); + }), +) {} diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/Sandbox.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/Sandbox.ts new file mode 100644 index 00000000000..64e46d292db --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/Sandbox.ts @@ -0,0 +1,87 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import { Stack } from "alchemy/Stack"; +import * as Effect from "effect/Effect"; +import type { PlatformError } from "effect/PlatformError"; +import * as Stream from "effect/Stream"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import * as ChildProcess from "effect/unstable/process/ChildProcess"; +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"; + +export class Sandbox extends Cloudflare.Container< + Sandbox, + { + /** + * Execute a command in a sandbox. + */ + exec: (command: string) => Effect.Effect< + { + exitCode: number; + stdout: string; + stderr: string; + }, + PlatformError + >; + } +>()( + "Sandbox", + Stack.useSync((stack) => ({ + main: import.meta.filename, + instanceType: stack.stage === "prod" ? "standard-1" : "dev", + observability: { + logs: { + enabled: true, + }, + }, + })), +) {} + +export const SandboxLive = /* @__PURE__ */ Sandbox.make( + Effect.gen(function* () { + // + const cp = yield* ChildProcessSpawner; + + let counter = 0; + + return Sandbox.of({ + exec: (command) => + cp + .spawn( + ChildProcess.make(command, { + shell: true, + }), + ) + .pipe( + Effect.flatMap((handle) => + Effect.all( + [ + handle.exitCode, + handle.stdout.pipe(Stream.decodeText, Stream.mkString), + handle.stderr.pipe(Stream.decodeText, Stream.mkString), + ], + { concurrency: "unbounded" }, + ), + ), + Effect.map(([exitCode, stdout, stderr]) => ({ + exitCode, + stdout, + stderr, + })), + Effect.scoped, + ), + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.url, "http://localhost"); + + if (url.pathname === "/increment") { + counter++; + return yield* HttpServerResponse.json({ counter }); + } + + return HttpServerResponse.text("Hello from Sandbox container!"); + }), + }); + }), +); + +export default SandboxLive; diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/SecondaryApi.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/SecondaryApi.ts new file mode 100644 index 00000000000..b99e4c66363 --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/SecondaryApi.ts @@ -0,0 +1,34 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import Agent from "./Agent.ts"; + +// A second Worker that binds the same `Agent` Durable Object as `Api`. Each +// `yield* Agent` runs the DO's outer init, which calls +// `Cloudflare.Container.bind(Sandbox)` and pushes a binding onto the Sandbox +// ContainerApplication carrying the Agent namespace. With two Workers binding +// the same DO, the Sandbox ends up with two bindings that share a single +// `namespaceId` — the regression case for the dedupe fix in this PR. +export class SecondaryApi extends Cloudflare.Worker()( + "SecondaryApi", + { + main: import.meta.filename, + observability: { enabled: true }, + }, +) {} + +export default SecondaryApi.make( + Effect.gen(function* () { + const agents = yield* Agent; + + return { + fetch: Effect.gen(function* () { + const body = yield* agents + .getByName("sandbox-test") + .hello() + .pipe(Effect.orDie); + return HttpServerResponse.text(body); + }), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/src/WorkerTag.ts b/.repos/alchemy-effect/examples/cloudflare-worker/src/WorkerTag.ts new file mode 100644 index 00000000000..7c20ef7e08b --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/src/WorkerTag.ts @@ -0,0 +1,29 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +// Tagged Worker DX: declare the class first (lightweight identifier), +// then provide the runtime implementation in a second `.make()` call. +// This mirrors the pattern in the README/docstring on Cloudflare.Worker. +export class WorkerTag extends Cloudflare.Worker()("WorkerTag", { + main: import.meta.filename, + compatibility: { + flags: ["nodejs_compat"], + date: "2026-04-26", + }, + observability: { + enabled: true, + }, +}) {} + +export default WorkerTag.make( + Effect.gen(function* () { + return { + fetch: Effect.gen(function* () { + return HttpServerResponse.text("Hello from WorkerTag", { + status: 200, + }); + }), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/test/integ.test.ts b/.repos/alchemy-effect/examples/cloudflare-worker/test/integ.test.ts new file mode 100644 index 00000000000..a5e40cac15f --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/test/integ.test.ts @@ -0,0 +1,193 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Bun"; +import { expect } from "bun:test"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as HttpBody from "effect/unstable/http/HttpBody"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "../alchemy.run.ts"; +import { WORKFLOW_SECRET_VALUE } from "../src/NotifyWorkflow.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), + stage: "test", + // dev: true, +}); + +// This stack deploys a Container (Sandbox) whose image build + push can take +// well over the default 120s hook budget, so give deploy/destroy more room. +const stack = beforeAll(deploy(Stack), { timeout: 600_000 }); + +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), { + timeout: 600_000, +}); + +test( + "integ", + Effect.gen(function* () { + const { url } = yield* stack; + + expect(url).toBeString(); + }), +); + +/** + * Regression guard for https://github.com/alchemy-run/alchemy-effect/pull/172 + * + * The stack now includes two Workers (`Api` and `SecondaryApi`) that both + * bind the same `Agent` Durable Object, which in turn binds the `Sandbox` + * Container. Each `yield* Agent` runs the DO's outer init, calling + * `Cloudflare.Container.bind(Sandbox)` once per Worker, so the Sandbox + * ContainerApplication receives two bindings sharing one `namespaceId`. + * + * Before the dedupe fix, `getDurableObjects` counted those as two distinct + * namespaces and the deploy in `beforeAll` died with: + * + * "A Container can only be bound to one Durable Object namespace. + * Found 2 namespaces in bindings: , " + * + * If the deploy ever starts failing again, the whole suite stops at + * `beforeAll` — that is the regression signal. This case just asserts the + * second Worker showed up with a URL so a silent regression that drops the + * binding still surfaces here. + */ +test( + "two workers binding the same container deploy without dedup error", + Effect.gen(function* () { + const { secondaryApiUrl } = yield* stack; + expect(secondaryApiUrl).toBeString(); + }), +); + +/** + * Regression guard for https://github.com/alchemy-run/alchemy-effect/pull/71 + * + * `NotifyWorkflow` accesses `Cloudflare.WorkerEnvironment` inside its body and + * performs a KV roundtrip via `env.KV.put` / `env.KV.get`. If the fix from #71 + * is ever reverted, the body Effect loses the `WorkerEnvironment` service and + * dies with `Service not found: Cloudflare.Workers.WorkerEnvironment` on the + * first `yield* Cloudflare.WorkerEnvironment` — the workflow instance never + * reaches `complete`, and this test times out or surfaces the `errored` status. + */ +test( + "workflow body can access WorkerEnvironment and exercise env bindings", + Effect.gen(function* () { + const { url } = yield* stack; + const roomId = `smoke-${Date.now()}`; + + // Start the workflow instance. + const startResponse = yield* Effect.tryPromise({ + try: () => fetch(`${url}/workflow/start/${roomId}`, { method: "POST" }), + catch: (cause) => + cause instanceof Error ? cause : new Error(String(cause)), + }); + expect(startResponse.status).toBe(200); + + const { instanceId } = yield* Effect.tryPromise({ + try: () => startResponse.json() as Promise<{ instanceId: string }>, + catch: (cause) => + cause instanceof Error ? cause : new Error(String(cause)), + }); + expect(instanceId).toBeString(); + + // Poll status until complete / errored / timeout (~60s). + const deadline = Date.now() + 60_000; + let lastStatus: + | { status: string; output?: unknown; error?: unknown } + | undefined; + while (Date.now() < deadline) { + const statusResponse = yield* Effect.tryPromise({ + try: () => fetch(`${url}/workflow/status/${instanceId}`), + catch: (cause) => + cause instanceof Error ? cause : new Error(String(cause)), + }); + expect(statusResponse.status).toBe(200); + lastStatus = yield* Effect.tryPromise({ + try: () => + statusResponse.json() as Promise<{ + status: string; + output?: unknown; + error?: unknown; + }>, + catch: (cause) => + cause instanceof Error ? cause : new Error(String(cause)), + }); + if (lastStatus.status === "complete" || lastStatus.status === "errored") { + break; + } + yield* Effect.sleep("2 seconds"); + } + + // The workflow must have completed — if WorkerEnvironment provision breaks, + // the body dies on the first yield and the instance never reaches complete. + expect(lastStatus).toBeDefined(); + expect(lastStatus!.status).toBe("complete"); + expect(lastStatus!.error).toBeFalsy(); + + // Prove the `Alchemy.Secret(...)` bound at plantime made it all the + // way through to the workflow body's runtime read. The workflow body + // unwraps `Redacted.value(secret)` and embeds it in the returned + // `processed` payload. + const output = lastStatus!.output as { secret?: string } | undefined; + expect(output?.secret).toBe(Redacted.value(WORKFLOW_SECRET_VALUE)); + }), + { timeout: 120_000 }, +); + +/** + * Queue producer→consumer round-trip via the Effect-style + * `Cloudflare.messages(Queue).subscribe(...)` API. + * + * Producer: `POST /queue/send` returns `{ sent: { id, text, sentAt } }` + * after enqueuing a message. + * + * Consumer: the worker's queue() handler (registered via subscribe in + * src/Api.ts) writes the message body to R2 at `/queue/`. The + * route `GET /queue/result/` reads it back. Cloudflare's queue + * dispatch is async and best-effort, so we poll for up to 60s. + */ +test( + "queue producer→consumer round-trip via messages().subscribe()", + Effect.gen(function* () { + const { url } = yield* stack; + const text = `hello-${Date.now()}`; + + const sendResponse = yield* HttpClient.execute( + HttpClientRequest.post(`${url}/queue/send`).pipe( + HttpClientRequest.setBody(HttpBody.text(text)), + ), + ); + expect(sendResponse.status).toBe(202); + const { sent } = (yield* sendResponse.json) as { + sent: { id: string; text: string; sentAt: number }; + }; + expect(sent.id).toBeTypeOf("string"); + + const deadline = Date.now() + 60_000; + let consumed: { id: string; text: string; sentAt: number } | undefined; + while (Date.now() < deadline) { + const resultResponse = yield* HttpClient.get( + `${url}/queue/result/${sent.id}`, + ); + if (resultResponse.status === 200) { + consumed = (yield* resultResponse.json) as typeof consumed; + break; + } + yield* Effect.sleep("2 seconds"); + } + + expect(consumed).toBeDefined(); + expect(consumed!.id).toBe(sent.id); + expect(consumed!.text).toBe(text); + + // Clean up the consumed R2 entry so afterAll's stack.destroy() + // can delete the bucket — otherwise Cloudflare rejects the + // bucket delete with "bucket is not empty". + yield* HttpClient.execute( + HttpClientRequest.make("DELETE")(`${url}/queue/result/${sent.id}`), + ); + }), + { timeout: 120_000 }, +); diff --git a/.repos/alchemy-effect/examples/cloudflare-worker/tsconfig.json b/.repos/alchemy-effect/examples/cloudflare-worker/tsconfig.json new file mode 100644 index 00000000000..484e2f0a26a --- /dev/null +++ b/.repos/alchemy-effect/examples/cloudflare-worker/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["alchemy.run.ts", "src/**/*.ts", "test/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext" + }, + "references": [ + { + "path": "../../packages/alchemy/tsconfig.json" + }, + { + "path": "../../packages/better-auth/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/.gitignore b/.repos/alchemy-effect/examples/monorepo-multi-stack/.gitignore new file mode 100644 index 00000000000..ac24b669bab --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +lib/ +.alchemy/ +.wrangler/ +*.tsbuildinfo diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/_package.json b/.repos/alchemy-effect/examples/monorepo-multi-stack/_package.json new file mode 100644 index 00000000000..7070585c88b --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/_package.json @@ -0,0 +1,30 @@ +{ + "name": "example-monorepo-multi-stack", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alchemy-run/alchemy-effect" + }, + "type": "module", + "scripts": { + "build": "tsc -b" + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:" + }, + "workspaces": { + "packages": ["frontend", "backend"], + "catalog": { + "@effect/platform-bun": "4.0.0-beta.60", + "@effect/platform-node": "4.0.0-beta.60", + "@types/bun": "latest", + "alchemy": "file:../packages/alchemy", + "effect": "4.0.0-beta.60", + "typescript": "^6.0.3", + "vite": "^8.0.7" + } + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/alchemy.run.ts b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/alchemy.run.ts new file mode 100644 index 00000000000..149c9654260 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/alchemy.run.ts @@ -0,0 +1,17 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import Service from "./src/Service.ts"; +import { Backend } from "./src/Stack.ts"; + +export default Backend.make( + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const api = yield* Service; + return { + url: api.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/package.json b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/package.json new file mode 100644 index 00000000000..b26a336e86b --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/package.json @@ -0,0 +1,42 @@ +{ + "name": "@monorepo-multi-stack/backend", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/monorepo-multi-stack/backend" + }, + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + }, + "exports": { + ".": { + "bun": "./src/index.ts", + "types": "./lib/index.d.ts", + "import": "./lib/index.js", + "default": "./lib/index.js" + }, + "./Client": { + "bun": "./src/Client.ts", + "types": "./lib/Client.d.ts", + "import": "./lib/Client.js", + "default": "./lib/Client.js" + } + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Client.ts b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Client.ts new file mode 100644 index 00000000000..943115e7254 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Client.ts @@ -0,0 +1,5 @@ +import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; +import { BackendApi } from "./Spec.ts"; + +export const BackendClient = (baseUrl: string) => + HttpApiClient.make(BackendApi, { baseUrl }); diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Service.ts b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Service.ts new file mode 100644 index 00000000000..97b11d4aa0a --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Service.ts @@ -0,0 +1,30 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Etag from "effect/unstable/http/Etag"; +import * as HttpPlatform from "effect/unstable/http/HttpPlatform"; +import * as HttpRouter from "effect/unstable/http/HttpRouter"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; +import { BackendApi, Greeting } from "./Spec.ts"; + +export default class Service extends Cloudflare.Worker()( + "Service", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const helloGroup = HttpApiBuilder.group(BackendApi, "Hello", (handlers) => + handlers.handle("hello", () => + Effect.succeed(new Greeting({ message: "Hello World" })), + ), + ); + + return { + fetch: HttpApiBuilder.layer(BackendApi).pipe( + Layer.provide(helloGroup), + Layer.provide([HttpPlatform.layer, Etag.layer]), + HttpRouter.toHttpEffect, + ), + }; + }), +) {} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Spec.ts b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Spec.ts new file mode 100644 index 00000000000..60dc73b60e9 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Spec.ts @@ -0,0 +1,16 @@ +import * as Schema from "effect/Schema"; +import * as HttpApi from "effect/unstable/httpapi/HttpApi"; +import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint"; +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup"; + +export class Greeting extends Schema.Class("Greeting")({ + message: Schema.String, +}) {} + +export const hello = HttpApiEndpoint.get("hello", "/", { + success: Greeting, +}); + +export class HelloGroup extends HttpApiGroup.make("Hello").add(hello) {} + +export class BackendApi extends HttpApi.make("BackendApi").add(HelloGroup) {} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Stack.ts b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Stack.ts new file mode 100644 index 00000000000..4493b51f675 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/Stack.ts @@ -0,0 +1,8 @@ +import * as Alchemy from "alchemy"; + +export class Backend extends Alchemy.Stack< + Backend, + { + url: string; + } +>()("Backend") {} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/index.ts b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/index.ts new file mode 100644 index 00000000000..166885251bf --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/src/index.ts @@ -0,0 +1,3 @@ +export * from "./Client.ts"; +export * from "./Spec.ts"; +export * from "./Stack.ts"; diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/tsconfig.json b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/tsconfig.json new file mode 100644 index 00000000000..1a6405a5f99 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/backend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "types": ["bun"], + "composite": true, + "noEmit": false, + "outDir": "./lib", + "rootDir": "./src" + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/alchemy.run.ts b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/alchemy.run.ts new file mode 100644 index 00000000000..715e3354875 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/alchemy.run.ts @@ -0,0 +1,26 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import { Backend } from "@monorepo-multi-stack/backend"; +import * as Effect from "effect/Effect"; + +export default Alchemy.Stack( + "Frontend", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + // reference the prod stage of the backend + const backend = yield* Backend; + + const website = yield* Cloudflare.Vite("Website", { + env: { + VITE_API_URL: backend.url, + }, + }); + + return { + url: website.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/index.html b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/index.html new file mode 100644 index 00000000000..26fad4c8c6b --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/index.html @@ -0,0 +1,11 @@ + + + + + Frontend + + +
+ + + diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/package.json b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/package.json new file mode 100644 index 00000000000..6874e88f8cf --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "@monorepo-multi-stack/frontend", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/monorepo-multi-stack/frontend" + }, + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.14", + "alchemy": "workspace:*", + "@monorepo-multi-stack/backend": "workspace:*", + "effect": "catalog:", + "react-dom": "^19.2.6", + "react": "^19.2.6", + "vite": "catalog:" + }, + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/src/main.tsx b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/src/main.tsx new file mode 100644 index 00000000000..3af84f43917 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/src/main.tsx @@ -0,0 +1,40 @@ +import { BackendClient } from "@monorepo-multi-stack/backend/Client"; +import * as Effect from "effect/Effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import React from "react"; +import ReactDOM from "react-dom/client"; + +const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8787"; + +const client = BackendClient(API_URL).pipe( + Effect.provide(FetchHttpClient.layer), +); + +function App() { + const [message, setMessage] = React.useState("loading…"); + + React.useEffect(() => { + client + .pipe( + Effect.flatMap((client) => client.Hello.hello()), + Effect.map((greeting) => greeting.message), + Effect.runPromise, + ) + .then(setMessage, (err) => setMessage(`error: ${String(err)}`)); + }, []); + + return ( +
+

{message}

+

+ Edit src/main.tsx and redeploy. +

+
+ ); +} + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/tsconfig.json b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/tsconfig.json new file mode 100644 index 00000000000..6a5669c3973 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/frontend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src"], + "references": [{ "path": "../backend" }], + "compilerOptions": { + "types": ["bun", "vite/client"], + "composite": true, + "noEmit": true + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/tsconfig.base.json b/.repos/alchemy-effect/examples/monorepo-multi-stack/tsconfig.base.json new file mode 100644 index 00000000000..6ed87e427e5 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/tsconfig.base.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "module": "Preserve", + "moduleResolution": "Bundler", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-multi-stack/tsconfig.json b/.repos/alchemy-effect/examples/monorepo-multi-stack/tsconfig.json new file mode 100644 index 00000000000..f763d3b3dd3 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-multi-stack/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./backend" }, { "path": "./frontend" }] +} diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/.gitignore b/.repos/alchemy-effect/examples/monorepo-single-stack/.gitignore new file mode 100644 index 00000000000..ac24b669bab --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +lib/ +.alchemy/ +.wrangler/ +*.tsbuildinfo diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/_package.json b/.repos/alchemy-effect/examples/monorepo-single-stack/_package.json new file mode 100644 index 00000000000..e6d6c3f2230 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/_package.json @@ -0,0 +1,36 @@ +{ + "name": "example-monorepo-single-stack", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alchemy-run/alchemy-effect" + }, + "type": "module", + "scripts": { + "build": "tsc -b" + }, + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "catalog:", + "effect": "catalog:" + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:" + }, + "workspaces": { + "packages": ["frontend", "backend"], + "catalog": { + "@effect/platform-bun": "4.0.0-beta.60", + "@effect/platform-node": "4.0.0-beta.60", + "@types/bun": "latest", + "alchemy": "file:../packages/alchemy", + "effect": "4.0.0-beta.60", + "typescript": "^6.0.3", + "vite": "^8.0.7" + } + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/alchemy.run.ts b/.repos/alchemy-effect/examples/monorepo-single-stack/alchemy.run.ts new file mode 100644 index 00000000000..1123a15d9a3 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/alchemy.run.ts @@ -0,0 +1,28 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { Path } from "effect/Path"; +import Service from "./backend/src/Service.ts"; + +export default Alchemy.Stack( + "Monorepo", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const backend = yield* Service; + const path = yield* Path; + + const website = yield* Cloudflare.Vite("Website", { + rootDir: path.resolve(import.meta.dirname, "frontend"), + env: { + VITE_API_URL: backend.url.as(), + }, + }); + return { + backendUrl: backend.url.as(), + websiteUrl: website.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/backend/package.json b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/package.json new file mode 100644 index 00000000000..17f45eb758e --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/package.json @@ -0,0 +1,42 @@ +{ + "name": "@monorepo-single-stack/backend", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/monorepo-single-stack/backend" + }, + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "workspace:*", + "effect": "catalog:" + }, + "exports": { + ".": { + "bun": "./src/index.ts", + "types": "./lib/index.d.ts", + "import": "./lib/index.js", + "default": "./lib/index.js" + }, + "./Client": { + "bun": "./src/Client.ts", + "types": "./lib/Client.d.ts", + "import": "./lib/Client.js", + "default": "./lib/Client.js" + } + }, + "scripts": { + "build": "tsc -b", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Client.ts b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Client.ts new file mode 100644 index 00000000000..943115e7254 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Client.ts @@ -0,0 +1,5 @@ +import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; +import { BackendApi } from "./Spec.ts"; + +export const BackendClient = (baseUrl: string) => + HttpApiClient.make(BackendApi, { baseUrl }); diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Service.ts b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Service.ts new file mode 100644 index 00000000000..97b11d4aa0a --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Service.ts @@ -0,0 +1,30 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Etag from "effect/unstable/http/Etag"; +import * as HttpPlatform from "effect/unstable/http/HttpPlatform"; +import * as HttpRouter from "effect/unstable/http/HttpRouter"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; +import { BackendApi, Greeting } from "./Spec.ts"; + +export default class Service extends Cloudflare.Worker()( + "Service", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const helloGroup = HttpApiBuilder.group(BackendApi, "Hello", (handlers) => + handlers.handle("hello", () => + Effect.succeed(new Greeting({ message: "Hello World" })), + ), + ); + + return { + fetch: HttpApiBuilder.layer(BackendApi).pipe( + Layer.provide(helloGroup), + Layer.provide([HttpPlatform.layer, Etag.layer]), + HttpRouter.toHttpEffect, + ), + }; + }), +) {} diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Spec.ts b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Spec.ts new file mode 100644 index 00000000000..60dc73b60e9 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/Spec.ts @@ -0,0 +1,16 @@ +import * as Schema from "effect/Schema"; +import * as HttpApi from "effect/unstable/httpapi/HttpApi"; +import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint"; +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup"; + +export class Greeting extends Schema.Class("Greeting")({ + message: Schema.String, +}) {} + +export const hello = HttpApiEndpoint.get("hello", "/", { + success: Greeting, +}); + +export class HelloGroup extends HttpApiGroup.make("Hello").add(hello) {} + +export class BackendApi extends HttpApi.make("BackendApi").add(HelloGroup) {} diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/index.ts b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/index.ts new file mode 100644 index 00000000000..c0b2e2b15ba --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/src/index.ts @@ -0,0 +1,2 @@ +export * from "./Client.ts"; +export * from "./Spec.ts"; diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/backend/tsconfig.json b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/tsconfig.json new file mode 100644 index 00000000000..1a6405a5f99 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/backend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "types": ["bun"], + "composite": true, + "noEmit": false, + "outDir": "./lib", + "rootDir": "./src" + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/index.html b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/index.html new file mode 100644 index 00000000000..26fad4c8c6b --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/index.html @@ -0,0 +1,11 @@ + + + + + Frontend + + +
+ + + diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/package.json b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/package.json new file mode 100644 index 00000000000..914de6eb052 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "@monorepo-single-stack/frontend", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "examples/monorepo-single-stack/frontend" + }, + "dependencies": { + "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.14", + "alchemy": "workspace:*", + "@monorepo-single-stack/backend": "workspace:*", + "effect": "catalog:", + "react-dom": "^19.2.6", + "react": "^19.2.6", + "vite": "catalog:" + }, + "scripts": { + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "logs": "alchemy logs", + "tail": "alchemy tail", + "test": "bun test" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/src/main.tsx b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/src/main.tsx new file mode 100644 index 00000000000..8b817fb8c64 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/src/main.tsx @@ -0,0 +1,40 @@ +import { BackendClient } from "@monorepo-single-stack/backend/Client"; +import * as Effect from "effect/Effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import React from "react"; +import ReactDOM from "react-dom/client"; + +const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8787"; + +const client = BackendClient(API_URL).pipe( + Effect.provide(FetchHttpClient.layer), +); + +function App() { + const [message, setMessage] = React.useState("loading…"); + + React.useEffect(() => { + client + .pipe( + Effect.flatMap((client) => client.Hello.hello()), + Effect.map((greeting) => greeting.message), + Effect.runPromise, + ) + .then(setMessage, (err) => setMessage(`error: ${String(err)}`)); + }, []); + + return ( +
+

{message}

+

+ Edit src/main.tsx and redeploy. +

+
+ ); +} + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/tsconfig.json b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/tsconfig.json new file mode 100644 index 00000000000..6a5669c3973 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/frontend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src"], + "references": [{ "path": "../backend" }], + "compilerOptions": { + "types": ["bun", "vite/client"], + "composite": true, + "noEmit": true + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/tsconfig.base.json b/.repos/alchemy-effect/examples/monorepo-single-stack/tsconfig.base.json new file mode 100644 index 00000000000..6ed87e427e5 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/tsconfig.base.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "module": "Preserve", + "moduleResolution": "Bundler", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + } +} diff --git a/.repos/alchemy-effect/examples/monorepo-single-stack/tsconfig.json b/.repos/alchemy-effect/examples/monorepo-single-stack/tsconfig.json new file mode 100644 index 00000000000..f763d3b3dd3 --- /dev/null +++ b/.repos/alchemy-effect/examples/monorepo-single-stack/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./backend" }, { "path": "./frontend" }] +} diff --git a/.repos/alchemy-effect/images/alchemy-effect-layers.png b/.repos/alchemy-effect/images/alchemy-effect-layers.png new file mode 100644 index 00000000000..01051ed5719 Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect-layers.png differ diff --git a/.repos/alchemy-effect/images/alchemy-effect-output.png b/.repos/alchemy-effect/images/alchemy-effect-output.png new file mode 100644 index 00000000000..37c805a9390 Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect-output.png differ diff --git a/.repos/alchemy-effect/images/alchemy-effect-plan-type.png b/.repos/alchemy-effect/images/alchemy-effect-plan-type.png new file mode 100644 index 00000000000..c6f94608694 Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect-plan-type.png differ diff --git a/.repos/alchemy-effect/images/alchemy-effect-plan.gif b/.repos/alchemy-effect/images/alchemy-effect-plan.gif new file mode 100644 index 00000000000..4efd152aab5 Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect-plan.gif differ diff --git a/.repos/alchemy-effect/images/alchemy-effect-policy-error.png b/.repos/alchemy-effect/images/alchemy-effect-policy-error.png new file mode 100644 index 00000000000..80be88e5031 Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect-policy-error.png differ diff --git a/.repos/alchemy-effect/images/alchemy-effect-triad.png b/.repos/alchemy-effect/images/alchemy-effect-triad.png new file mode 100644 index 00000000000..d82b2c53fcd Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect-triad.png differ diff --git a/.repos/alchemy-effect/images/alchemy-effect-triple.png b/.repos/alchemy-effect/images/alchemy-effect-triple.png new file mode 100644 index 00000000000..f791fa78e0b Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect-triple.png differ diff --git a/.repos/alchemy-effect/images/alchemy-effect.gif b/.repos/alchemy-effect/images/alchemy-effect.gif new file mode 100644 index 00000000000..95ea4db3f64 Binary files /dev/null and b/.repos/alchemy-effect/images/alchemy-effect.gif differ diff --git a/.repos/alchemy-effect/images/readme-hero.png b/.repos/alchemy-effect/images/readme-hero.png new file mode 100644 index 00000000000..9ece6546ffd Binary files /dev/null and b/.repos/alchemy-effect/images/readme-hero.png differ diff --git a/.repos/alchemy-effect/package.json b/.repos/alchemy-effect/package.json new file mode 100644 index 00000000000..47b537ed46e --- /dev/null +++ b/.repos/alchemy-effect/package.json @@ -0,0 +1,142 @@ +{ + "name": "alchemy-monorepo", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/alchemy-run/alchemy-effect" + }, + "type": "module", + "module": "./lib/index.js", + "scripts": { + "audit:service": "bun scripts/audit-service.ts", + "build:clean": "bun clean . && bun i && bun run build && bun download:env", + "build:clean:shallow": "bun clean:shallow . && bun run build", + "build": "bun tsc -b && bun run --filter ./website --filter './packages/*' build", + "build:packages": "bun tsc -b && bun run --filter './packages/*' build", + "bump": "bun ./scripts/release/bump.ts", + "clean:shallow": "rm -rf alchemy/*.tgz && bun tsc -b --clean", + "clean": "git clean -fqdx -e .env", + "dev": "tsc -b -w & bun run --filter alchemy dev", + "dev:website": "cd ./website && bun astro dev", + "deploy:website": "cd ./website && bun run build:reference && bun alchemy deploy", + "deploy:website:prod": "bun deploy:website --stage prod --profile prod", + "destroy:website": "cd ./website && bun alchemy destroy", + "destroy:website:prod": "bun destroy:website --stage prod --profile prod", + "deploy:github": "doppler run -c dev --project alchemy-v2 -- bun alchemy deploy ./stacks/github.ts --profile admin", + "logs:otel": "doppler run -c dev --project alchemy-v2 -- bun alchemy logs ./stacks/otel.ts", + "logs:otel:prod": "bun logs:otel --profile prod --stage prod", + "deploy:otel": "doppler run -c dev --project alchemy-v2 -- bun alchemy deploy ./stacks/otel.ts", + "deploy:otel:prod": "bun deploy:otel --profile prod --stage prod", + "destroy:github": "doppler run -c dev --project alchemy-v2 -- bun alchemy destroy ./stacks/github.ts --profile admin", + "destroy:otel": "doppler run -c dev --project alchemy-v2 -- bun alchemy destroy ./stacks/otel.ts --profile admin", + "destroy:otel:prod": "bun destroy:otel --profile prod --stage prod", + "download:cfn": "bun scripts/generate-cfn-docs.ts", + "download:env": "doppler secrets download --project alchemy-v2 --config dev --no-file --format env > .env", + "download:external": "bun download:env && bun download:cfn && bun download:terraform", + "format:check": "bun run format -- --check", + "format": "oxfmt '.'", + "generate:api-reference": "bun scripts/generate-api-reference.ts", + "generate:docs": "bun scripts/generate-docs.ts", + "install:clean": "rm -rf node_modules examples/**/node_modules alchemy/node_modules && bun i", + "nuke:aws:run": "bash scripts/nuke-aws.sh --no-dry-run --no-prompt --prompt-delay 3", + "nuke:aws": "bash scripts/nuke-aws.sh", + "opencode": "OPENCODE_EXPERIMENTAL_OXFMT=true opencode", + "publish:npm": "bun run build && bun run --filter alchemy publish:npm", + "setup": "effect-language-service patch && bun run download:external", + "sync:submodules": "git submodule sync --recursive && git submodule update --init --recursive", + "test:benchmark": "bun run --filter alchemy test:benchmark", + "test:examples": "bun run --filter ./examples/cloudflare-worker --filter ./examples/cloudflare-worker-async --filter ./examples/cloudflare-tanstack --filter ./examples/cloudflare-tanstack-start-solid --filter ./examples/aws-lambda test && bun format", + "test:smoke": "bun test --only-failures ./test/smoke.test.ts", + "test:canary": "SMOKE_CANARY=1 bun test --only-failures ./test/smoke.test.ts", + "test:fast": "FAST=1 bun run test:live", + "test:live": "bun ./scripts/test.ts", + "test:local": "LOCAL=1 bun ./scripts/test.ts", + "test": "bun run test:live", + "prepare": "husky" + }, + "workspaces": { + "packages": [ + "website", + "packages/*", + "examples/*", + "examples/monorepo-single-stack/*", + "examples/monorepo-multi-stack/*" + ], + "catalog": { + "@alchemy.run/node-utils": "0.0.4", + "@aws-sdk/credential-providers": "^3.0.0", + "@cloudflare/containers": "^0.1.1", + "@cloudflare/vite-plugin": "^1.13.12", + "@cloudflare/workers-types": "^4.20250805.0", + "@distilled.cloud/aws": "^0.22.4", + "@distilled.cloud/axiom": "^0.22.4", + "@distilled.cloud/cloudflare-rolldown-plugin": "0.10.1", + "@distilled.cloud/cloudflare-runtime": "0.10.1", + "@distilled.cloud/cloudflare-vite-plugin": "0.10.1", + "@distilled.cloud/cloudflare": "^0.22.4", + "@distilled.cloud/core": "^0.22.4", + "@distilled.cloud/neon": "^0.22.4", + "@distilled.cloud/planetscale": "^0.22.4", + "@effect/language-service": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/platform-bun": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/platform-node-shared": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/platform-node": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/sql-pg": ">=4.0.0-beta.74 || >=4.0.0", + "@effect/vitest": ">=4.0.0-beta.74 || >=4.0.0", + "@libsql/client": "^0.17.0", + "@opentui/core": "latest", + "@opentui/solid": "latest", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "@types/aws-lambda": "^8.10.152", + "@types/bun": "latest", + "@types/node": "latest", + "@typescript/native-preview": "latest", + "ai": "^6.0.62", + "aws4fetch": "^1.0.20", + "better-auth": "^1.6.2", + "drizzle-kit": ">=1.0.0-rc.1", + "drizzle-orm": ">=1.0.0-rc.1", + "effect": ">=4.0.0-beta.74 || >=4.0.0", + "fast-xml-parser": "^5.3.4", + "rolldown": "1.0.1", + "solid-js": "latest", + "sonda": "^0.11.1", + "typescript": "^6.0.3", + "vite": "^8.0.7", + "web-tree-sitter": "0.25.10", + "ws": "^8.20.0", + "yaml": "^2.0.0" + } + }, + "devDependencies": { + "@alchemy.run/pr-package": "workspace:*", + "@ark/attest": "^0.56.0", + "@cloudflare/puppeteer": "^1.1.0", + "@distilled.cloud/cloudflare": "catalog:", + "@effect/language-service": "^0.77.0", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@types/bun": "latest", + "@types/node": "latest", + "@typescript/native-preview": "^7.0.0-dev.20260317.1", + "@vitest/ui": "^4.1.0", + "alchemy": "workspace:*", + "bun-types": "^1.3.8", + "changelogithub": "^13.16.1", + "dotenv": "^17.2.3", + "effect": "catalog:", + "husky": "^9.1.7", + "oxfmt": "latest", + "oxlint": "latest", + "pkg-pr-new": "^0.0.62", + "ts-morph": "^27.0.2", + "typescript": "latest", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "^4.0.18", + "yaml": "^2.8.2" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/packages/alchemy/.gitignore b/.repos/alchemy-effect/packages/alchemy/.gitignore new file mode 100644 index 00000000000..d36977dc476 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/.gitignore @@ -0,0 +1 @@ +.tmp diff --git a/.repos/alchemy-effect/packages/alchemy/bin/alchemy.sh b/.repos/alchemy-effect/packages/alchemy/bin/alchemy.sh new file mode 100755 index 00000000000..255fb685c20 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/bin/alchemy.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Auto-generated by scripts/hoist-alchemy-bin.ts. Do not edit. +# Dev-only shim: runs alchemy.ts under bun, falls back to alchemy.js under node. +RUNTIME=node +case "$npm_execpath" in + *bun*) RUNTIME=bun ;; +esac +if [ "$RUNTIME" = "node" ] && command -v bun >/dev/null 2>&1; then + RUNTIME=bun +fi +if [ "$RUNTIME" = "bun" ]; then + exec bun "/Users/samgoodwin/workspaces/alchemy-effect/packages/alchemy/bin/alchemy.ts" "$@" +else + exec node "/Users/samgoodwin/workspaces/alchemy-effect/packages/alchemy/bin/alchemy.js" "$@" +fi diff --git a/.repos/alchemy-effect/packages/alchemy/bin/alchemy.ts b/.repos/alchemy-effect/packages/alchemy/bin/alchemy.ts new file mode 100644 index 00000000000..e5eeb041967 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/bin/alchemy.ts @@ -0,0 +1,5 @@ +import { runMain } from "alchemy/Util/PlatformServices"; + +import { main } from "alchemy/Cli"; + +main.pipe(runMain); diff --git a/.repos/alchemy-effect/packages/alchemy/bin/cli.js b/.repos/alchemy-effect/packages/alchemy/bin/cli.js new file mode 100755 index 00000000000..19392f61101 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/bin/cli.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// alchemy CLI launcher +// +// Resolves the alchemy CLI entrypoint via node module resolution and execs it +// under whichever runtime the user invoked us with. The shebang forces this +// launcher to run as node even when bun was the invoker, but bun forwards +// signals about itself via env vars on every child it spawns: +// +// - `npm_execpath` → path to bun (set for `bun run + + +`; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/fixtures/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/fixtures/worker.ts new file mode 100644 index 00000000000..d4875496dc2 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/fixtures/worker.ts @@ -0,0 +1,3 @@ +export default { + fetch: async () => new Response("ok"), +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/.gitignore b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/.gitignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/.gitignore @@ -0,0 +1 @@ +dist diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/build.sh b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/build.sh new file mode 100755 index 00000000000..d2ab3a84f97 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Minimal deterministic build for the StaticSite fixture: copy src/ -> dist/. +# +# We sleep briefly before *and* during the copy so that any Worker.update +# that races against this build (e.g. reading dist/ mid-write) has a wide +# enough window to actually trip. The user-visible bug this test guards +# against — "two deploys needed to publish assets" — only manifests when +# the build takes meaningful wall time, which a real astro/vite build +# does but a one-line `cp` does not. +set -euo pipefail +sleep 0.5 +rm -rf dist +mkdir -p dist +sleep 0.5 +cp -R src/. dist/ diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/src/index.html b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/src/index.html new file mode 100644 index 00000000000..ca66e96e30d --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/staticsite-fixture/src/index.html @@ -0,0 +1,9 @@ + + + + StaticSite fixture v1 + + +

StaticSite fixture v1

+ + diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/.gitignore b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/.gitignore new file mode 100644 index 00000000000..de4d1f007dd --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/index.html b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/index.html new file mode 100644 index 00000000000..e8d5df1fb0e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/index.html @@ -0,0 +1,11 @@ + + + + + Vite fixture v1 + + +
Vite fixture v1
+ + + diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/package.json b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/package.json new file mode 100644 index 00000000000..a5e07d0453b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/package.json @@ -0,0 +1,6 @@ +{ + "name": "alchemy-vite-fixture", + "version": "0.0.0", + "private": true, + "type": "module" +} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/src/main.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/src/main.ts new file mode 100644 index 00000000000..ad5b9c89299 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/src/main.ts @@ -0,0 +1,15 @@ +// Minimal client entry. The cloudflare-vite-plugin handles the worker +// runtime separately; this file just satisfies the `index.html` script tag +// reference so Vite emits a real client bundle. +// +// `import.meta.env.VITE_TEST_MARKER` is referenced here so the +// "Vite: env props" test can verify `Cloudflare.Vite({ env })` actually +// reaches the client bundle. Vite inlines the value via the `define` +// hook at build time; the integration test fetches the deployed JS +// asset and asserts the value is present. +const marker = (import.meta.env as { VITE_TEST_MARKER?: string }) + .VITE_TEST_MARKER; +const el = document.getElementById("app"); +if (el) { + el.textContent = `${el.textContent} (hydrated, marker=${marker ?? ""})`; +} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/src/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/src/worker.ts new file mode 100644 index 00000000000..10859e02ad8 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/src/worker.ts @@ -0,0 +1,12 @@ +// Minimal worker entry. The cloudflare-vite-plugin wraps this in a +// virtual module and emits it as the SSR bundle. We delegate to the +// asset binding (`ASSETS.fetch`) so the actual fixture content (the +// built `index.html`) is what's served — that's what the test is +// checking on subsequent deploys. +type Env = { ASSETS: { fetch: (req: Request) => Promise } }; + +export default { + fetch(request: Request, env: Env): Promise { + return env.ASSETS.fetch(request); + }, +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/vite.config.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/vite.config.ts new file mode 100644 index 00000000000..0225f8ea146 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/vite-fixture/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; + +// `Cloudflare.Vite` declares an `ssr` environment but doesn't set a +// worker entry by default. For non-framework projects (no React/Vue +// plugin to inject one), Vite 8 errors out with "rollupOptions.input +// should not be an html file when building for SSR". We point the SSR +// build at our minimal worker entry so the cloudflare-vite-plugin can +// wrap it into the worker bundle. +export default defineConfig({ + environments: { + ssr: { + build: { + rollupOptions: { + input: "./src/worker.ts", + }, + }, + }, + }, +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/CronEventSource.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/CronEventSource.test.ts new file mode 100644 index 00000000000..c5e56b1196f --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/CronEventSource.test.ts @@ -0,0 +1,85 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Alchemy from "@/index.ts"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import CronTestWorker from "./fixtures/cron-worker.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const Stack = Alchemy.Stack( + "CronEventSourceStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* CronTestWorker; + return { + url: worker.url.as(), + crons: worker.crons, + }; + }), +); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test.skipIf(!!process.env.NO_SLOW_TESTS)( + "deployed worker fires the scheduled handler on its cron trigger", + Effect.gen(function* () { + const { url, crons } = yield* stack; + expect(crons).toContain("* * * * *"); + + const client = yield* HttpClient.HttpClient; + + // Reset any leftover state from prior runs. Doubles as a readiness probe — + // a fresh workers.dev URL can take a few seconds to start serving 200s. + yield* Effect.gen(function* () { + const res = yield* client.post(`${url}/reset`); + if (res.status !== 200) { + return yield* Effect.fail(new Error(`Worker not ready: ${res.status}`)); + } + }).pipe( + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 10, + }), + ); + const resetAt = Date.now(); + + // Cloudflare cron granularity is one minute and there's some propagation + // delay after deploy, so we poll up to ~3 minutes for the first fire. + const times = yield* Effect.gen(function* () { + const res = yield* client.get(`${url}/times`); + if (res.status !== 200) return []; + const body = (yield* res.json) as { times?: unknown }; + if (!Array.isArray(body.times)) return []; + return body.times.filter((t) => t >= resetAt); + }).pipe( + Effect.catch(() => Effect.succeed([])), + Effect.repeat({ + schedule: Schedule.spaced("5 seconds"), + until: (recent) => recent.length > 0, + times: 36, + }), + ); + + expect(times.length).toBeGreaterThan(0); + for (const t of times) { + expect(t).toBeGreaterThanOrEqual(resetAt); + } + }).pipe(logLevel), + { timeout: 300_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/DurableObjectNamespace.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/DurableObjectNamespace.test.ts new file mode 100644 index 00000000000..7b62a44e86c --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/DurableObjectNamespace.test.ts @@ -0,0 +1,350 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as Stream from "effect/Stream"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import Stack from "./fixtures/do-rpc/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +// Cap exponential backoff at 3s — keeps the fast-path snappy but stops +// the geometric blow-up (0.5 + 1 + 2 + 4 + 8 + 16 + 32 + 64s ...) that +// makes retries dominate test wall time when CF edge is slow. +const readinessSchedule = Schedule.exponential("500 millis").pipe( + Schedule.either(Schedule.spaced("3 seconds")), +); + +const readinessRetries = 15; + +// The test runtime's HttpClient (FetchHttpClient/undici) keeps HTTP/1.1 +// connections alive and pooled. A pooled connection stays pinned to a single +// Cloudflare edge metal, so when that metal lags the freshly-deployed version +// every retry rides the same stale socket and keeps reading the old body for +// the life of the keep-alive — even though the new version is already live. +// Forcing `Connection: close` makes each readiness attempt open a fresh +// connection, letting it land on an edge that has the new version (this is +// why a brand-new `curl` sees the update immediately while a kept-alive +// client does not). See do-rpc DurableObjectNamespace test investigation. +const freshConn = HttpClient.mapRequest( + HttpClientRequest.setHeader("connection", "close"), +); + +test( + "durable object methods can use binding clients", + Effect.gen(function* () { + const { url } = yield* stack; + const client = yield* HttpClient.HttpClient; + + const res = yield* client.post(`${url}/roundtrip`).pipe( + Effect.flatMap((res) => + res.status === 200 + ? Effect.succeed(res) + : Effect.fail(new Error(`Worker not ready: ${res.status}`)), + ), + Effect.retry({ schedule: readinessSchedule, times: 15 }), + ); + + expect(res.status).toBe(200); + const body = (yield* res.json) as { value: string }; + expect(body.value).toBe("ok"); + }).pipe(logLevel), + { timeout: 60_000 }, +); + +// Reproduces the `tick` streaming example from the Durable Objects tutorial: +// https://v2.alchemy.run/tutorial/cloudflare/durable-objects/ +// +// The DO exposes `tick(n): Stream` and the Worker forwards it to the +// HTTP response with `HttpServerResponse.stream`. The client reads the body as +// newline-delimited integers. With `/tick/5` we expect ["0","1","2","3","4"]. +test( + "tick streams sequential values from a durable object (tutorial repro)", + Effect.gen(function* () { + const { url } = yield* stack; + const client = freshConn(yield* HttpClient.HttpClient); + + const lines = yield* client.get(`${url}/tick/5`).pipe( + Effect.flatMap((res) => + res.status === 200 + ? res.stream.pipe( + Stream.decodeText, + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.runCollect, + Effect.map((chunk) => [...chunk]), + ) + : Effect.fail(new Error(`Worker not ready: ${res.status}`)), + ), + Effect.retry({ schedule: readinessSchedule, times: readinessRetries }), + ); + + expect(lines).toEqual(["0", "1", "2", "3", "4"]); + }).pipe(logLevel), + { timeout: 60_000 }, +); + +// While a freshly pre-created worker is propagating, Cloudflare's edge +// serves Alchemy's pre-create stub, which responds 200 with this plain-text +// body. It is not the real script, so any poll that sees it must retry. +const DEPLOY_PLACEHOLDER = "Alchemy worker is being deployed..."; + +// Cloudflare's edge keeps serving the previous worker version (or the +// pre-create stub) for a while after a (re)deploy, so retrying on 200-only +// is not enough — the stale version still returns 200 with the old body. +// Retry until the body matches the expected version string. +const fetchReady = (url: string, expected: string) => + Effect.gen(function* () { + const client = freshConn(yield* HttpClient.HttpClient); + return yield* client.get(url).pipe( + Effect.flatMap((r) => + r.status === 200 + ? Effect.flatMap(r.text, (body) => + body === expected + ? Effect.succeed(body) + : Effect.fail( + new Error(`stale: got ${body}, want ${expected}`), + ), + ) + : Effect.fail(new Error(`Worker not ready: ${r.status}`)), + ), + Effect.retry({ schedule: readinessSchedule, times: readinessRetries }), + ); + }); + +const fetchJsonReady = (url: string) => + Effect.gen(function* () { + const client = freshConn(yield* HttpClient.HttpClient); + return yield* client.get(url).pipe( + // Parse the body INSIDE the retry: the pre-create stub answers 200 with + // a non-JSON placeholder, so JSON decoding must be part of the readiness + // check (a 200 status alone does not mean the real script is live yet). + Effect.flatMap((r) => + r.status !== 200 + ? Effect.fail(new Error(`Worker not ready: ${r.status}`)) + : Effect.flatMap(r.text, (body) => + body.includes(DEPLOY_PLACEHOLDER) + ? Effect.fail(new Error("stale: still deploying")) + : Effect.try({ + try: () => JSON.parse(body) as T, + catch: () => new Error(`non-json body: ${body}`), + }), + ), + ), + Effect.retry({ schedule: readinessSchedule, times: readinessRetries }), + ); + }); + +const hostWorkerScript = `import { DurableObject } from "cloudflare:workers"; +export class Counter extends DurableObject { + async increment() { + const value = (await this.ctx.storage.get("count")) ?? 0; + const next = value + 1; + await this.ctx.storage.put("count", next); + return next; + } + async reset() { + await this.ctx.storage.delete("count"); + } + async get() { + return (await this.ctx.storage.get("count")) ?? 0; + } +} +export default { + async fetch(request, env) { + const stub = env.Counter.getByName("shared"); + const url = new URL(request.url); + if (url.pathname === "/increment") { + return Response.json({ value: await stub.increment() }); + } + if (url.pathname === "/get") { + return Response.json({ value: await stub.get() }); + } + if (url.pathname === "/reset") { + await stub.reset(); + return Response.json({ ok: true }); + } + return new Response("Not Found", { status: 404 }); + }, +}; +`; + +const consumerWorkerScript = `export default { + async fetch(request, env) { + const stub = env.Counter.getByName("shared"); + const url = new URL(request.url); + if (url.pathname === "/increment") { + return Response.json({ value: await stub.increment() }); + } + if (url.pathname === "/get") { + return Response.json({ value: await stub.get() }); + } + if (url.pathname === "/reset") { + await stub.reset(); + return Response.json({ ok: true }); + } + return new Response("Not Found", { status: 404 }); + }, +}; +`; + +test.provider( + "async worker durable object binding accepts scriptName", + (scratch) => + Effect.gen(function* () { + yield* scratch.deploy( + Effect.gen(function* () { + return { + host: yield* Cloudflare.Worker("host-worker", { + script: hostWorkerScript, + env: { + Counter: Cloudflare.DurableObjectNamespace("Counter"), + }, + }), + }; + }), + ); + + const deployed = yield* scratch.deploy( + Effect.gen(function* () { + const host = yield* Cloudflare.Worker("host-worker", { + script: hostWorkerScript, + env: { + Counter: Cloudflare.DurableObjectNamespace("Counter"), + }, + }); + const consumer = yield* Cloudflare.Worker("consumer-worker", { + script: consumerWorkerScript, + env: { + Counter: Cloudflare.DurableObjectNamespace("Counter", { + scriptName: host.workerName, + }), + }, + }); + + return { consumer, host }; + }), + ); + + const reset = yield* fetchJsonReady<{ ok: boolean }>( + `${deployed.host.url}/reset`, + ); + expect(reset.ok).toBe(true); + + const first = yield* fetchJsonReady<{ value: number }>( + `${deployed.consumer.url}/increment`, + ); + expect(first.value).toBe(1); + + const second = yield* fetchJsonReady<{ value: number }>( + `${deployed.host.url}/get`, + ); + expect(second.value).toBe(1); + + yield* scratch.destroy(); + }).pipe(logLevel), + { timeout: 60_000 }, +); + +// Walk an async worker through four redeploys against the same scratch state, +// each one swapping in a new script + bindings shape so we exercise the +// migration paths `putWorker` relies on: +// v1 — create with a single DO class `DO_A` +// v2 — rename `DO_A` → `DO_A_v2` (className change, same binding id) +// v3 — add a brand-new DO class `DO_B` alongside `DO_A_v2` +// v4 — delete `DO_A`, keep only `DO_B` +test.provider( + "durable object class migrations across redeploys", + (scratch) => + Effect.gen(function* () { + const v1 = yield* scratch.deploy( + Effect.gen(function* () { + return { + worker: yield* Cloudflare.Worker("worker", { + script: `import { DurableObject } from "cloudflare:workers"; +export class DO_A extends DurableObject {} +export default { async fetch() { return new Response("v1"); } }; +`, + env: { + DO_A: Cloudflare.DurableObjectNamespace("DO_A"), + }, + }), + }; + }), + ); + expect(yield* fetchReady(v1.worker.url!, "v1")).toBe("v1"); + + const v2 = yield* scratch.deploy( + Effect.gen(function* () { + return { + worker: yield* Cloudflare.Worker("worker", { + script: `import { DurableObject } from "cloudflare:workers"; +export class DO_A_v2 extends DurableObject {} +export default { async fetch() { return new Response("v2"); } }; +`, + env: { + DO_A: Cloudflare.DurableObjectNamespace("DO_A", { + className: "DO_A_v2", + }), + }, + }), + }; + }), + ); + expect(yield* fetchReady(v2.worker.url!, "v2")).toBe("v2"); + + const v3 = yield* scratch.deploy( + Effect.gen(function* () { + return { + worker: yield* Cloudflare.Worker("worker", { + script: `import { DurableObject } from "cloudflare:workers"; +export class DO_A_v2 extends DurableObject {} +export class DO_B extends DurableObject {} +export default { async fetch() { return new Response("v3"); } }; +`, + env: { + DO_A: Cloudflare.DurableObjectNamespace("DO_A", { + className: "DO_A_v2", + }), + DO_B: Cloudflare.DurableObjectNamespace("DO_B"), + }, + }), + }; + }), + ); + expect(yield* fetchReady(v3.worker.url!, "v3")).toBe("v3"); + + const v4 = yield* scratch.deploy( + Effect.gen(function* () { + return { + worker: yield* Cloudflare.Worker("worker", { + script: `import { DurableObject } from "cloudflare:workers"; +export class DO_B extends DurableObject {} +export default { async fetch() { return new Response("v4"); } }; +`, + env: { + DO_B: Cloudflare.DurableObjectNamespace("DO_B"), + }, + }), + }; + }), + ); + expect(yield* fetchReady(v4.worker.url!, "v4")).toBe("v4"); + + yield* scratch.destroy(); + }).pipe(logLevel), + { timeout: 180_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/DynamicWorkerLoader.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/DynamicWorkerLoader.test.ts new file mode 100644 index 00000000000..fa1b4deaf91 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/DynamicWorkerLoader.test.ts @@ -0,0 +1,63 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import Stack from "./fixtures/dynamic-worker-loader/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{ + status: number; + body: string; +}> {} + +// Fresh workers.dev URLs serve Cloudflare's placeholder page for a few seconds +// after deploy. Retry until the worker (and its dynamically-loaded child) +// answer 200, surfacing the body if not so a real failure isn't masked. +const readJson = (url: string) => + HttpClient.HttpClient.pipe( + Effect.flatMap((client) => client.get(url)), + Effect.flatMap((res) => + res.status === 200 + ? res.json + : res.text.pipe( + Effect.flatMap((body) => + Effect.fail(new WorkerNotReady({ status: res.status, body })), + ), + ), + ), + Effect.retry({ + while: (e): e is WorkerNotReady => e instanceof WorkerNotReady, + schedule: Schedule.exponential("500 millis").pipe( + Schedule.both(Schedule.recurs(20)), + ), + }), + ); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "async worker loads and proxies to a dynamic worker via env binding", + Effect.gen(function* () { + const { asyncWorkerUrl } = yield* stack; + const body = yield* readJson(asyncWorkerUrl); + expect(body).toMatchObject({ mode: "async", ok: true }); + }), + { timeout: 180_000 }, +); + +test( + "effect worker loads and proxies to a dynamic worker via yield* loader", + Effect.gen(function* () { + const { effectWorkerUrl } = yield* stack; + const body = yield* readJson(effectWorkerUrl); + expect(body).toMatchObject({ mode: "effect", ok: true }); + }), + { timeout: 180_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/HttpApi.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/HttpApi.test.ts new file mode 100644 index 00000000000..055ca3cf51d --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/HttpApi.test.ts @@ -0,0 +1,225 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"; +import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; +import { TaskApi } from "./fixtures/http-api/api.ts"; +import Stack from "./fixtures/http-api/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const testTimeout = 60_000; +const burstTimeout = 90_000; +const requestTimeout = "5 seconds"; +// A fresh Cloudflare deploy is eventually consistent and NOT atomic across +// edge PoPs: the script, the workers.dev route, and each binding (R2 / D1 / +// DO namespace + migration) propagate independently. Until they converge, +// requests landing on a cold PoP return a `404` (route not resolvable) or a +// `500` (script up, binding not ready). New DO namespaces / D1 databases are +// the slowest, so the readiness window comfortably exceeds 15s in the tail. +// Retry on a steady 1.5s cadence for ~30s so every first-touch request rides +// out the convergence window regardless of which PoP it hits. +const readinessRetry = { + schedule: Schedule.spaced("1500 millis"), + times: 20, +} as const; + +const makeClient = (url: string) => + HttpApiClient.make(TaskApi, { baseUrl: url }); + +// The raw `HttpClient` (used for transport-level CORS checks) does not fail on +// a non-2xx status, so `Effect.retry` won't fire on the freshly-deployed edge +// 404/500 window. Explicitly `Effect.fail` non-2xx responses to force the +// retry (unlike the typed `HttpApiClient`, which already fails on them). +const requestUntilReady = ( + effect: Effect.Effect, +) => + effect.pipe( + Effect.timeout(requestTimeout), + Effect.flatMap( + Effect.fnUntraced(function* (res) { + return res.status >= 200 && res.status < 300 + ? res + : yield* Effect.fail( + new Error(`Worker not ready: ${res.status} ${yield* res.text}`), + ); + }), + ), + Effect.retry(readinessRetry), + ); + +// Gate the deploy on the worker having propagated to the edge: hit both the +// R2-backed (`createTask`) and DO-backed (`createTaskDO`) paths once, retrying +// through the freshly-deployed 404 window, then give it extra time to settle +// across edge PoPs. Individual tests can then call the API directly without +// each re-implementing a per-request readiness retry. +const stack = beforeAll( + deploy(Stack).pipe( + Effect.tap(({ url }) => + Effect.gen(function* () { + const client = yield* makeClient(url); + yield* client.Tasks.createTask({ payload: { title: "warmup" } }).pipe( + Effect.timeout(requestTimeout), + Effect.retry(readinessRetry), + ); + yield* client.Tasks.createTaskDO({ payload: { title: "warmup" } }).pipe( + Effect.timeout(requestTimeout), + Effect.retry(readinessRetry), + ); + }), + ), + // just give it some extra time to propagate + Effect.tap(Effect.sleep("5 seconds")), + ), +); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "deployed http-api worker handles createTask + getTask via typed HttpApiClient", + Effect.gen(function* () { + const { url } = yield* stack; + expect(url).toBeTypeOf("string"); + const client = yield* makeClient(url); + + const created = yield* client.Tasks.createTask({ + payload: { title: "Write docs" }, + }).pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + expect(created.title).toBe("Write docs"); + expect(created.completed).toBe(false); + expect(created.id).toBeTypeOf("string"); + + const fetched = yield* client.Tasks.getTask({ + params: { id: created.id }, + }).pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + expect(fetched.id).toBe(created.id); + expect(fetched.title).toBe("Write docs"); + + // No retry here: `TaskNotFound` is the expected domain result, not a + // transient readiness failure — retrying would just re-run the 404. + const missing = yield* client.Tasks.getTask({ + params: { id: "does-not-exist" }, + }).pipe(Effect.flip); + expect(missing._tag).toBe("TaskNotFound"); + if (missing._tag === "TaskNotFound") { + expect(missing.id).toBe("does-not-exist"); + } + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "cors middleware adds Access-Control-Allow-Origin header on preflight", + Effect.gen(function* () { + const { url } = yield* stack; + // CORS preflight (OPTIONS) is transport-level and not part of the typed + // HttpApi surface, so this single check uses the raw HttpClient. + const client = yield* HttpClient.HttpClient; + + const res = yield* requestUntilReady( + client.execute( + HttpClientRequest.make("OPTIONS")(url).pipe( + HttpClientRequest.setHeaders({ + Origin: "https://example.com", + "Access-Control-Request-Method": "POST", + "Access-Control-Request-Headers": "content-type", + }), + ), + ), + ); + expect(res.headers["access-control-allow-origin"]).toBeDefined(); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "cors middleware adds Access-Control-Allow-Origin header on actual requests", + Effect.gen(function* () { + const { url } = yield* stack; + const client = yield* HttpClient.HttpClient; + + const res = yield* requestUntilReady( + client.execute( + HttpClientRequest.post(`${url}/`).pipe( + HttpClientRequest.setHeaders({ Origin: "https://example.com" }), + HttpClientRequest.bodyJsonUnsafe({ title: "cors-check" }), + ), + ), + ); + expect(res.status).toBe(200); + expect(res.headers["access-control-allow-origin"]).toBeDefined(); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "concurrent createTask survives scope-lifecycle pressure", + Effect.gen(function* () { + const { url } = yield* stack; + const client = yield* makeClient(url); + + const N = 200; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + Effect.gen(function* () { + const created = yield* client.Tasks.createTask({ + payload: { title: `task-${i}` }, + }).pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + if (created.title !== `task-${i}`) { + return yield* Effect.fail( + new Error(`create ${i} title mismatch: ${created.title}`), + ); + } + return created.id; + }), + { concurrency: 64 }, + ); + + expect(results).toHaveLength(N); + expect(new Set(results).size).toBe(N); + }).pipe(logLevel), + { timeout: burstTimeout }, +); + +test( + "createTaskDO + getTaskDO round-trip 100x in parallel through the DO HttpApi", + Effect.gen(function* () { + const { url } = yield* stack; + const client = yield* makeClient(url); + + const N = 100; + yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + Effect.gen(function* () { + const title = `do-task-${i}`; + const created = yield* client.Tasks.createTaskDO({ + payload: { title }, + }).pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + expect(created.title).toBe(title); + expect(created.completed).toBe(false); + expect(created.id).toBeTypeOf("string"); + + const fetched = yield* client.Tasks.getTaskDO({ + params: { id: created.id }, + }).pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + expect(fetched.id).toBe(created.id); + expect(fetched.title).toBe(title); + }), + { concurrency: 32 }, + ); + }).pipe(logLevel), + { timeout: burstTimeout }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Rpc.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Rpc.test.ts new file mode 100644 index 00000000000..65eff0c6d4e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Rpc.test.ts @@ -0,0 +1,667 @@ +import { + RpcDecodeError, + RpcCallError, + RpcRemoteStreamError, + encodeRpcError, + decodeRpcResult, + decodeRpcValue, + ErrorTag, + StreamTag, + StreamErrorTag, + isRpcErrorEnvelope, + isRpcStreamEnvelope, + fromRpcReadableStream, + fromRpcStreamEnvelope, + toRpcStream, + makeRpcStub, + type RpcErrorEnvelope, + type RpcStreamEnvelope, +} from "@/Cloudflare/Workers/Rpc"; +import { describe, expect, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Stream from "effect/Stream"; + +class MyError extends Data.TaggedError("MyError")<{ + readonly message: string; +}> {} + +// --------------------------------------------------------------------------- +// RpcDecodeError +// --------------------------------------------------------------------------- + +describe("RpcDecodeError", () => { + it.effect("message delegates to Error.message when cause is an Error", () => + Effect.gen(function* () { + const err = new RpcDecodeError({ cause: new Error("inner") }); + expect(err._tag).toBe("RpcDecodeError"); + expect(err.message).toBe("inner"); + }), + ); + + it.effect("message stringifies non-Error cause", () => + Effect.gen(function* () { + const err = new RpcDecodeError({ cause: 42 }); + expect(err.message).toBe("42"); + }), + ); +}); + +// --------------------------------------------------------------------------- +// RpcCallError +// --------------------------------------------------------------------------- + +describe("RpcCallError", () => { + it.effect("message includes method name and Error.message", () => + Effect.gen(function* () { + const err = new RpcCallError({ + method: "doStuff", + cause: new Error("boom"), + }); + expect(err._tag).toBe("RpcCallError"); + expect(err.message).toBe('RPC call to "doStuff" failed: boom'); + }), + ); + + it.effect("message stringifies non-Error cause", () => + Effect.gen(function* () { + const err = new RpcCallError({ method: "doStuff", cause: "oops" }); + expect(err.message).toBe('RPC call to "doStuff" failed: oops'); + }), + ); +}); + +// --------------------------------------------------------------------------- +// isRpcErrorEnvelope +// --------------------------------------------------------------------------- + +describe("isRpcErrorEnvelope", () => { + it.effect("detects valid envelope", () => + Effect.gen(function* () { + const envelope: RpcErrorEnvelope = { + _tag: ErrorTag, + error: { message: "boom" }, + }; + expect(isRpcErrorEnvelope(envelope)).toBe(true); + }), + ); + + it.effect("rejects non-envelope values", () => + Effect.gen(function* () { + expect(isRpcErrorEnvelope(null)).toBe(false); + expect(isRpcErrorEnvelope(undefined)).toBe(false); + expect(isRpcErrorEnvelope(42)).toBe(false); + expect(isRpcErrorEnvelope("hello")).toBe(false); + expect(isRpcErrorEnvelope({ _tag: "Other" })).toBe(false); + expect(isRpcErrorEnvelope({ _tag: ErrorTag })).toBe(false); + }), + ); +}); + +// --------------------------------------------------------------------------- +// isRpcStreamEnvelope +// --------------------------------------------------------------------------- + +describe("isRpcStreamEnvelope", () => { + it.effect("detects valid bytes envelope", () => + Effect.gen(function* () { + const envelope: RpcStreamEnvelope = { + _tag: StreamTag, + encoding: "bytes", + body: new ReadableStream(), + }; + expect(isRpcStreamEnvelope(envelope)).toBe(true); + }), + ); + + it.effect("detects valid jsonl envelope", () => + Effect.gen(function* () { + const envelope: RpcStreamEnvelope = { + _tag: StreamTag, + encoding: "jsonl", + body: new ReadableStream(), + }; + expect(isRpcStreamEnvelope(envelope)).toBe(true); + }), + ); + + it.effect("rejects missing or wrong fields", () => + Effect.gen(function* () { + expect(isRpcStreamEnvelope(null)).toBe(false); + expect(isRpcStreamEnvelope(42)).toBe(false); + expect(isRpcStreamEnvelope({ _tag: StreamTag })).toBe(false); + expect( + isRpcStreamEnvelope({ + _tag: StreamTag, + encoding: "xml", + body: new ReadableStream(), + }), + ).toBe(false); + expect( + isRpcStreamEnvelope({ + _tag: StreamTag, + encoding: "jsonl", + body: "not a stream", + }), + ).toBe(false); + }), + ); +}); + +// --------------------------------------------------------------------------- +// encodeRpcError +// --------------------------------------------------------------------------- + +describe("encodeRpcError", () => { + it.effect("preserves tagged error fields", () => + Effect.gen(function* () { + const error = new MyError({ message: "BOOF" }); + const encoded = encodeRpcError(error) as Record; + expect(encoded._tag).toBe("MyError"); + expect(encoded.message).toBe("BOOF"); + }), + ); + + it.effect("normalizes plain Error to name/message/stack", () => + Effect.gen(function* () { + const error = new Error("plain failure"); + const encoded = encodeRpcError(error) as Record; + expect(encoded.name).toBe("Error"); + expect(encoded.message).toBe("plain failure"); + expect(encoded.stack).toBeDefined(); + }), + ); + + it.effect("passes through primitives", () => + Effect.gen(function* () { + expect(encodeRpcError("string error")).toBe("string error"); + expect(encodeRpcError(42)).toBe(42); + expect(encodeRpcError(null)).toBe(null); + expect(encodeRpcError(undefined)).toBe(undefined); + }), + ); + + it.effect("passes through plain objects", () => + Effect.gen(function* () { + const obj = { code: 404, detail: "not found" }; + expect(encodeRpcError(obj)).toBe(obj); + }), + ); +}); + +// --------------------------------------------------------------------------- +// Helper: create a ReadableStream from a string +// --------------------------------------------------------------------------- + +const textToReadableStream = (text: string): ReadableStream => { + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(text)); + controller.close(); + }, + }); +}; + +const bytesToReadableStream = ( + chunks: Uint8Array[], +): ReadableStream => + new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(chunk); + } + controller.close(); + }, + }); + +// --------------------------------------------------------------------------- +// fromRpcReadableStream +// --------------------------------------------------------------------------- + +describe("fromRpcReadableStream", () => { + it.effect("bytes encoding passes raw Uint8Array chunks through", () => + Effect.gen(function* () { + const data = new TextEncoder().encode("hello"); + const body = bytesToReadableStream([data]); + const stream = fromRpcReadableStream(body, "bytes"); + const chunks = yield* Stream.runCollect(stream); + expect(chunks).toEqual([data]); + }), + ); + + it.effect("jsonl encoding parses JSON lines", () => + Effect.gen(function* () { + const body = textToReadableStream('{"a":1}\n{"b":2}\n'); + const stream = fromRpcReadableStream(body, "jsonl"); + const chunks = yield* Stream.runCollect(stream); + expect(chunks).toEqual([{ a: 1 }, { b: 2 }]); + }), + ); + + it.effect("jsonl encoding produces RpcDecodeError on malformed JSON", () => + Effect.gen(function* () { + const body = textToReadableStream("not json\n"); + const stream = fromRpcReadableStream(body, "jsonl"); + const exit = yield* Effect.exit(Stream.runCollect(stream)); + expect(Exit.isFailure(exit)).toBe(true); + }), + ); + + it.effect("jsonl encoding skips empty lines", () => + Effect.gen(function* () { + const body = textToReadableStream('{"x":1}\n\n\n{"y":2}\n'); + const stream = fromRpcReadableStream(body, "jsonl"); + const chunks = yield* Stream.runCollect(stream); + expect(chunks).toEqual([{ x: 1 }, { y: 2 }]); + }), + ); +}); + +// --------------------------------------------------------------------------- +// fromRpcStreamEnvelope +// --------------------------------------------------------------------------- + +describe("fromRpcStreamEnvelope", () => { + it.effect("delegates to fromRpcReadableStream", () => + Effect.gen(function* () { + const body = textToReadableStream('{"v":99}\n'); + const envelope: RpcStreamEnvelope = { + _tag: StreamTag, + encoding: "jsonl", + body, + }; + const chunks = yield* Stream.runCollect(fromRpcStreamEnvelope(envelope)); + expect(chunks).toEqual([{ v: 99 }]); + }), + ); +}); + +// --------------------------------------------------------------------------- +// decodeRpcValue +// --------------------------------------------------------------------------- + +describe("decodeRpcValue", () => { + it.effect("passes through plain values", () => + Effect.gen(function* () { + expect(decodeRpcValue("hello")).toBe("hello"); + expect(decodeRpcValue(42)).toBe(42); + expect(decodeRpcValue(null)).toBe(null); + }), + ); + + it.effect("converts stream envelope to Effect Stream", () => + Effect.gen(function* () { + const body = textToReadableStream('{"k":1}\n'); + const envelope: RpcStreamEnvelope = { + _tag: StreamTag, + encoding: "jsonl", + body, + }; + const result = decodeRpcValue(envelope); + expect(Stream.isStream(result)).toBe(true); + const chunks = yield* Stream.runCollect(result as Stream.Stream); + expect(chunks).toEqual([{ k: 1 }]); + }), + ); + + it.effect("converts bare ReadableStream to bytes Effect Stream", () => + Effect.gen(function* () { + const data = new TextEncoder().encode("raw"); + const body = bytesToReadableStream([data]); + const result = decodeRpcValue(body); + expect(Stream.isStream(result)).toBe(true); + const chunks = yield* Stream.runCollect(result as Stream.Stream); + expect(chunks).toEqual([data]); + }), + ); +}); + +// --------------------------------------------------------------------------- +// decodeRpcResult +// --------------------------------------------------------------------------- + +describe("decodeRpcResult", () => { + it.effect("succeeds for plain values", () => + Effect.gen(function* () { + const result = yield* decodeRpcResult("hello"); + expect(result).toBe("hello"); + }), + ); + + it.effect("succeeds for numeric values", () => + Effect.gen(function* () { + const result = yield* decodeRpcResult(42); + expect(result).toBe(42); + }), + ); + + it.effect("fails for error envelopes with tagged error", () => + Effect.gen(function* () { + const envelope: RpcErrorEnvelope = { + _tag: ErrorTag, + error: { _tag: "MyError", message: "BOOF" }, + }; + const exit = yield* Effect.exit(decodeRpcResult(envelope)); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const failReason = exit.cause.reasons.find((r) => r._tag === "Fail"); + expect(failReason).toBeDefined(); + const error = (failReason as any).error as Record; + expect(error._tag).toBe("MyError"); + expect(error.message).toBe("BOOF"); + } + }), + ); + + it.effect("fails with plain Error shape for error envelopes", () => + Effect.gen(function* () { + const envelope: RpcErrorEnvelope = { + _tag: ErrorTag, + error: { name: "Error", message: "plain failure" }, + }; + const exit = yield* Effect.exit(decodeRpcResult(envelope)); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const failReason = exit.cause.reasons.find((r) => r._tag === "Fail"); + const error = (failReason as any).error as Record; + expect(error.message).toBe("plain failure"); + } + }), + ); + + it.effect("wraps stream envelopes in succeed (stream passthrough)", () => + Effect.gen(function* () { + const body = textToReadableStream('{"s":1}\n'); + const envelope: RpcStreamEnvelope = { + _tag: StreamTag, + encoding: "jsonl", + body, + }; + const result = yield* decodeRpcResult(envelope); + expect(Stream.isStream(result)).toBe(true); + const chunks = yield* Stream.runCollect(result as Stream.Stream); + expect(chunks).toEqual([{ s: 1 }]); + }), + ); +}); + +// --------------------------------------------------------------------------- +// toRpcStream +// --------------------------------------------------------------------------- + +describe("toRpcStream", () => { + it.effect("selects jsonl encoding for non-byte data", () => + Effect.gen(function* () { + const stream = Stream.fromIterable([{ a: 1 }]); + const envelope = yield* toRpcStream(stream); + expect(envelope._tag).toBe(StreamTag); + expect(envelope.encoding).toBe("jsonl"); + expect(envelope.body).toBeInstanceOf(ReadableStream); + }), + ); + + it.effect("roundtrips a single-element jsonl stream", () => + Effect.gen(function* () { + const stream = Stream.fromIterable([{ a: 1 }]); + const envelope = yield* toRpcStream(stream); + const decoded = fromRpcReadableStream(envelope.body, "jsonl"); + const chunks = yield* Stream.runCollect(decoded); + expect(chunks).toEqual([{ a: 1 }]); + }), + ); + + it.effect("selects bytes encoding for Uint8Array data", () => + Effect.gen(function* () { + const data = new Uint8Array([1, 2, 3]); + const stream = Stream.fromIterable([data]); + const envelope = yield* toRpcStream(stream); + expect(envelope._tag).toBe(StreamTag); + expect(envelope.encoding).toBe("bytes"); + expect(envelope.body).toBeInstanceOf(ReadableStream); + }), + ); + + it.effect("roundtrips a single-element bytes stream", () => + Effect.gen(function* () { + const data = new Uint8Array([1, 2, 3]); + const stream = Stream.fromIterable([data]); + const envelope = yield* toRpcStream(stream); + const decoded = fromRpcReadableStream(envelope.body, "bytes"); + const chunks = yield* Stream.runCollect(decoded); + expect(chunks).toEqual([data]); + }), + ); + + it.effect("handles empty stream as jsonl", () => + Effect.gen(function* () { + const stream = Stream.empty; + const envelope = yield* toRpcStream(stream); + expect(envelope._tag).toBe(StreamTag); + expect(envelope.encoding).toBe("jsonl"); + + const decoded = fromRpcReadableStream(envelope.body, envelope.encoding); + const chunks = yield* Stream.runCollect(decoded); + expect(chunks).toEqual([]); + }), + ); +}); + +// --------------------------------------------------------------------------- +// makeRpcStub +// --------------------------------------------------------------------------- + +describe("makeRpcStub", () => { + it.effect("proxies successful calls", () => + Effect.gen(function* () { + const mockStub = { + greet: async (name: string) => `hello ${name}`, + }; + const stub = makeRpcStub<{ + greet: (name: string) => Effect.Effect; + }>(mockStub); + + const result = yield* stub.greet("world"); + expect(result).toBe("hello world"); + }), + ); + + it.effect("wraps rejected promises as RpcCallError", () => + Effect.gen(function* () { + const mockStub = { + boom: async () => { + throw new Error("kaboom"); + }, + }; + const stub = makeRpcStub<{ + boom: () => Effect.Effect; + }>(mockStub); + + const exit = yield* Effect.exit(stub.boom()); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const failReason = exit.cause.reasons.find((r) => r._tag === "Fail"); + expect(failReason).toBeDefined(); + const error = (failReason as any).error; + expect(error._tag).toBe("RpcCallError"); + expect(error.method).toBe("boom"); + } + }), + ); + + it.effect("decodes error envelopes into Effect.fail", () => + Effect.gen(function* () { + const mockStub = { + failMe: async () => ({ + _tag: ErrorTag, + error: { _tag: "MyError", message: "remote fail" }, + }), + }; + const stub = makeRpcStub<{ + failMe: () => Effect.Effect; + }>(mockStub); + + const exit = yield* Effect.exit(stub.failMe()); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const failReason = exit.cause.reasons.find((r) => r._tag === "Fail"); + const error = (failReason as any).error as Record; + expect(error._tag).toBe("MyError"); + expect(error.message).toBe("remote fail"); + } + }), + ); + + it.effect("decodes stream envelopes from successful calls", () => + Effect.gen(function* () { + const body = textToReadableStream('{"n":42}\n'); + const mockStub = { + streamMe: async () => ({ + _tag: StreamTag, + encoding: "jsonl" as const, + body, + }), + }; + const stub = makeRpcStub<{ + streamMe: () => Effect.Effect>; + }>(mockStub); + + const stream = yield* stub.streamMe(); + expect(Stream.isStream(stream)).toBe(true); + const chunks = yield* Stream.runCollect(stream); + expect(chunks).toEqual([{ n: 42 }]); + }), + ); +}); + +// --------------------------------------------------------------------------- +// Stream error transport +// --------------------------------------------------------------------------- + +describe("stream errors", () => { + it.effect("toRpcStream encodes a stream that fails immediately", () => + Effect.gen(function* () { + const stream = Stream.fail(new MyError({ message: "immediate" })); + const envelope = yield* toRpcStream(stream); + expect(envelope._tag).toBe(StreamTag); + expect(envelope.encoding).toBe("jsonl"); + + const decoded = fromRpcReadableStream(envelope.body, "jsonl"); + const exit = yield* Effect.exit(Stream.runCollect(decoded)); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const reason = exit.cause.reasons.find(Cause.isFailReason); + expect(reason).toBeDefined(); + const err = reason!.error; + expect(err).toBeInstanceOf(RpcRemoteStreamError); + expect((err as RpcRemoteStreamError).error).toEqual({ + _tag: "MyError", + message: "immediate", + }); + } + }), + ); + + it.effect("toRpcStream encodes a stream that fails after elements", () => + Effect.gen(function* () { + const stream = Stream.make(1, 2).pipe( + Stream.concat(Stream.fail(new MyError({ message: "mid" }))), + ); + const envelope = yield* toRpcStream(stream); + expect(envelope._tag).toBe(StreamTag); + expect(envelope.encoding).toBe("jsonl"); + + const decoded = fromRpcReadableStream(envelope.body, "jsonl"); + const exit = yield* Effect.exit(Stream.runCollect(decoded)); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const reason = exit.cause.reasons.find(Cause.isFailReason); + expect(reason).toBeDefined(); + const err = reason!.error; + expect(err).toBeInstanceOf(RpcRemoteStreamError); + expect((err as RpcRemoteStreamError).error).toEqual({ + _tag: "MyError", + message: "mid", + }); + } + }), + ); + + it.effect("fromRpcReadableStream decodes error marker in JSONL", () => + Effect.gen(function* () { + const errorLine = JSON.stringify({ + _tag: StreamErrorTag, + error: { _tag: "MyError", message: "wire" }, + }); + const body = textToReadableStream(`${errorLine}\n`); + const stream = fromRpcReadableStream(body, "jsonl"); + const exit = yield* Effect.exit(Stream.runCollect(stream)); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const reason = exit.cause.reasons.find(Cause.isFailReason); + const err = reason!.error; + expect(err).toBeInstanceOf(RpcRemoteStreamError); + expect((err as RpcRemoteStreamError).error).toEqual({ + _tag: "MyError", + message: "wire", + }); + } + }), + ); + + it.effect("fromRpcReadableStream yields elements before error marker", () => + Effect.gen(function* () { + const errorLine = JSON.stringify({ + _tag: StreamErrorTag, + error: { message: "after elements" }, + }); + const body = textToReadableStream(`{"v":1}\n{"v":2}\n${errorLine}\n`); + const stream = fromRpcReadableStream(body, "jsonl"); + const collected: unknown[] = []; + const exit = yield* Effect.exit( + Stream.runForEach(stream, (item) => + Effect.sync(() => { + collected.push(item); + }), + ), + ); + expect(collected).toEqual([{ v: 1 }, { v: 2 }]); + expect(Exit.isFailure(exit)).toBe(true); + }), + ); + + it.effect( + "makeRpcStub preserves stream errors (not collapsed to RpcCallError)", + () => + Effect.gen(function* () { + const errorLine = JSON.stringify({ + _tag: StreamErrorTag, + error: { _tag: "MyError", message: "remote stream err" }, + }); + const body = textToReadableStream(`{"n":1}\n${errorLine}\n`); + const mockStub = { + streamFail: async () => ({ + _tag: StreamTag, + encoding: "jsonl" as const, + body, + }), + }; + const stub = makeRpcStub<{ + streamFail: () => Effect.Effect< + Stream.Stream + >; + }>(mockStub); + + const stream = yield* stub.streamFail(); + const exit = yield* Effect.exit(Stream.runCollect(stream)); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const reason = exit.cause.reasons.find(Cause.isFailReason); + expect(reason).toBeDefined(); + expect(reason!.error).toBeInstanceOf(RpcRemoteStreamError); + } + }), + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcDurableObjectNamespace.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcDurableObjectNamespace.test.ts new file mode 100644 index 00000000000..49a465d0129 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcDurableObjectNamespace.test.ts @@ -0,0 +1,303 @@ +import { expect } from "@effect/vitest"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as Stream from "effect/Stream"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as RpcClient from "effect/unstable/rpc/RpcClient"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import Stack from "./fixtures/rpc-do-namespace-do-rpc/stack.ts"; +import { WorkerRpcs as RpcWorkerWorkerRpcs } from "./fixtures/rpc-worker-rpc-http/group.ts"; +import RpcWorkerStack from "./fixtures/rpc-worker-rpc-http/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +// Cap exponential backoff at 3s so retries stay bounded when CF edge is +// slow (otherwise the geometric blow-up dominates wall time). +const readinessSchedule = Schedule.exponential("500 millis").pipe( + Schedule.either(Schedule.spaced("3 seconds")), +); + +// Suffix DO instance ids with a per-process random tag so reruns under +// `NO_DESTROY=1` don't collide with persisted state from earlier runs +// (the DO's `count` lives in `state.storage`). +const runId = Math.random().toString(36).slice(2, 10); +const k = (name: string) => `${name}-${runId}`; + +const resetCounter = (url: string, id: string) => + Effect.gen(function* () { + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + yield* client + .post(`${url}/counter/${id}/reset`) + .pipe(Effect.retry({ schedule: readinessSchedule, times: 10 })); + }); + +const rpcClientLayer = (url: string) => + RpcClient.layerProtocolHttp({ url }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide( + Layer.succeed(RpcSerialization.RpcSerialization, RpcSerialization.ndjson), + ), + ); + +const readinessRetries = 15; + +// The `*DO` RPC handlers forward to the Durable Object via `getByName(...)`. +// On a freshly-deployed worker the DO-namespace binding hasn't propagated to +// every Cloudflare edge yet, so the first calls fail with `Worker not found.`. +// The worker fixture wraps the DO call in `Effect.orDie` / `Stream.orDie`, so +// that error arrives at the client as a DEFECT — and `Effect.retry` does not +// retry defects. Promote defects to failures so the readiness retry can absorb +// the transient binding-propagation error (a genuine bug would simply keep +// failing until the retry budget is exhausted). +const retryReadyN = + (times: number) => + (eff: Effect.Effect) => + eff.pipe( + Effect.catchDefect((defect) => Effect.fail(defect)), + Effect.retry({ schedule: readinessSchedule, times }), + ); + +const retryReady = retryReadyN(readinessRetries); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +// Gate the deploy on the worker→DO binding having propagated to the edge: +// hit both the unary and streaming `*DO` paths once, retrying through the +// transient `Worker not found.` window, so individual tests can call the +// `*DO` RPCs directly without each having to re-implement the readiness retry. +const rpcWorkerStack = beforeAll( + deploy(RpcWorkerStack).pipe( + Effect.tap((outputs) => + Effect.gen(function* () { + const c = yield* RpcClient.make(RpcWorkerWorkerRpcs); + yield* c.PingDO({ message: "warmup" }).pipe(retryReady); + yield* c.CountDO({ upto: 1 }).pipe(Stream.runCollect, retryReady); + }).pipe(Effect.scoped, Effect.provide(rpcClientLayer(outputs.url))), + ), + // just give it some extra time to propagate + Effect.tap(Effect.sleep("5 seconds")), + ), +); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(RpcWorkerStack)); + +test( + "RpcDurableObjectNamespace: Increment / Get round-trip via Worker", + Effect.gen(function* () { + const { url } = yield* stack; + const alpha = k("alpha"); + yield* resetCounter(url, alpha); + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + + const incRes = yield* client.post(`${url}/counter/${alpha}/increment`); + expect(incRes.status).toBe(200); + const inc = (yield* incRes.json) as { count: number }; + expect(inc.count).toBe(1); + + yield* client.post(`${url}/counter/${alpha}/increment`); + yield* client.post(`${url}/counter/${alpha}/increment`); + + const getRes = yield* client.get(`${url}/counter/${alpha}`); + expect(getRes.status).toBe(200); + const got = (yield* getRes.json) as { count: number }; + expect(got.count).toBe(3); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcDurableObjectNamespace: separate getByName(id) instances are isolated", + Effect.gen(function* () { + const { url } = yield* stack; + const betaId = k("beta"); + const gammaId = k("gamma"); + yield* resetCounter(url, betaId); + yield* resetCounter(url, gammaId); + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + + yield* client.post(`${url}/counter/${betaId}/increment`); + + const beta = (yield* (yield* client.get(`${url}/counter/${betaId}`)) + .json) as { + count: number; + }; + const gamma = (yield* (yield* client.get(`${url}/counter/${gammaId}`)) + .json) as { + count: number; + }; + expect(beta.count).toBe(1); + expect(gamma.count).toBe(0); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcDurableObjectNamespace: streaming RPC via getByName(id).CountUpTo", + Effect.gen(function* () { + const { url } = yield* stack; + const delta = k("delta"); + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + + const res = yield* client + .get(`${url}/counter/${delta}/stream?upto=4`) + .pipe(Effect.retry({ times: 5 })); + expect(res.status).toBe(200); + const body = yield* res.text; + const lines = body.split("\n").filter((l) => l.length > 0); + expect(lines).toEqual(["1", "2", "3", "4"]); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcWorker + RpcDurableObjectNamespace: Worker proxies *DO RPCs through the typed namespace", + Effect.gen(function* () { + const { url } = yield* rpcWorkerStack; + + yield* Effect.gen(function* () { + const c = yield* RpcClient.make(RpcWorkerWorkerRpcs); + const ping = yield* c.Ping({ message: "hi" }).pipe(retryReady); + expect(ping.echo).toBe("hi"); + + const pingDO = yield* c.PingDO({ message: "via DO" }).pipe(retryReady); + expect(pingDO.echo).toBe("via DO"); + }).pipe(Effect.scoped, Effect.provide(rpcClientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcDurableObjectNamespace: 100 concurrent Increment calls do not hang", + Effect.gen(function* () { + const { url } = yield* stack; + const concurrent = k("concurrent"); + yield* resetCounter(url, concurrent); + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + + yield* client.post(`${url}/counter/${concurrent}/increment`); + + const N = 100; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + () => + client.post(`${url}/counter/${concurrent}/increment`).pipe( + Effect.flatMap((res) => res.json), + Effect.timeout("10 seconds"), + Effect.retry({ + schedule: readinessSchedule, + times: 3, + }), + ), + { concurrency: 32 }, + ); + + expect(results).toHaveLength(N); + const finalRes = yield* client.get(`${url}/counter/${concurrent}`); + const final = (yield* finalRes.json) as { count: number }; + expect(final.count).toBe(N + 1); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcWorker + RpcDurableObjectNamespace: 100 concurrent unary RPCs do not hang", + Effect.gen(function* () { + const { url } = yield* rpcWorkerStack; + + yield* Effect.gen(function* () { + const c = yield* RpcClient.make(RpcWorkerWorkerRpcs); + + const N = 100; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + c.Ping({ message: `m-${i}` }).pipe( + Effect.timeout("10 seconds"), + Effect.retry({ + schedule: readinessSchedule, + times: 3, + }), + ), + { concurrency: 32 }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i].echo).toBe(`m-${i}`); + } + }).pipe(Effect.scoped, Effect.provide(rpcClientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcWorker + RpcDurableObjectNamespace: 100 concurrent *DO unary RPCs do not hang", + Effect.gen(function* () { + const { url } = yield* rpcWorkerStack; + + yield* Effect.gen(function* () { + const c = yield* RpcClient.make(RpcWorkerWorkerRpcs); + + const N = 100; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + c + .PingDO({ message: `m-${i}` }) + .pipe(Effect.timeout("10 seconds"), retryReadyN(5)), + { concurrency: 16 }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i].echo).toBe(`m-${i}`); + } + }).pipe(Effect.scoped, Effect.provide(rpcClientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcWorker + RpcDurableObjectNamespace: 100 concurrent streaming *DO RPCs do not hang", + Effect.gen(function* () { + const { url } = yield* rpcWorkerStack; + + yield* Effect.gen(function* () { + const c = yield* RpcClient.make(RpcWorkerWorkerRpcs); + + const N = 100; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + c + .CountDO({ upto: 3 + (i % 3) }) + .pipe( + Stream.runCollect, + Effect.timeout("10 seconds"), + retryReadyN(5), + ), + { concurrency: 16 }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i]).toEqual( + Array.from({ length: 3 + (i % 3) }, (_, n) => n + 1), + ); + } + }).pipe(Effect.scoped, Effect.provide(rpcClientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcHttp.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcHttp.test.ts new file mode 100644 index 00000000000..149926152ce --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcHttp.test.ts @@ -0,0 +1,297 @@ +import { expect } from "@effect/vitest"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Vitest"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as Stream from "effect/Stream"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as RpcClient from "effect/unstable/rpc/RpcClient"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import { WorkerRpcs } from "./fixtures/rpc-http/group.ts"; +import Stack from "./fixtures/rpc-http/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const stack = beforeAll( + deploy(Stack).pipe( + // Ping the Worker to ensure it's ready. + // Subsequent calls should succeed without retries. + Effect.tap(({ url }) => + Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + const result = yield* client.Ping({ message: "warmup" }).pipe( + Effect.tapError(Console.log), + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 5, + }), + ); + expect(result.echo).toBe("warmup"); + expect(result.n).toBeGreaterThan(0); + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))), + ), + ), +); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +// The Cloudflare Worker fetch adapter (`workersHttpHandler`) currently +// short-circuits Effect's standard HTTP lifecycle (it manually +// provides `HttpServerRequest` and converts the response to a web +// `Response` outside of `HttpEffect.toHandled`). PR #328 reported that +// this can deadlock `RpcServer.toHttpEffect` under workerd. This test +// hammers a real deployed Worker exposing an Effect RPC group to +// surface lifecycle / per-request scope regressions. +// +// The `*DO` variants exercise the DO fetch pathway +// (`DurableObjectBridge.fetch` -> `makeRequestEffect`) via an +// `RpcClient` constructed inside the Worker handler whose transport +// is `Cloudflare.toHttpClient(rpcDO.getByName(...))`. This mirrors the +// HttpApi fixture's `getTaskDO` pattern. +const clientLayer = (url: string) => + RpcClient.layerProtocolHttp({ url }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide( + Layer.succeed(RpcSerialization.RpcSerialization, RpcSerialization.ndjson), + ), + ); + +test( + "RpcServer.toHttpEffect: unary RPC response", + Effect.gen(function* () { + const { url } = yield* stack; + console.log("url:", url); + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + const result = yield* client + .Ping({ message: "hello" }) + .pipe(Effect.tapError(Console.log)); + expect(result.echo).toBe("hello"); + expect(result.n).toBeGreaterThan(0); + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcServer.toHttpEffect: streaming RPC response", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + const values = yield* client.Count({ upto: 5 }).pipe(Stream.runCollect); + expect(values).toEqual([1, 2, 3, 4, 5]); + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcServer.toHttpEffect: array payload streams response items in order", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + const messages = ["a", "b", "c", "d"]; + const values = yield* client.Echo({ messages }).pipe(Stream.runCollect); + expect(values).toEqual( + messages.map((message, index) => ({ index, message })), + ); + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcServer.toHttpEffect: 200 concurrent unary calls do not hang", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + + const N = 200; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + client.Ping({ message: `m-${i}` }).pipe( + Effect.timeout("5 seconds"), + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 3, + }), + ), + { concurrency: 64 }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i].echo).toBe(`m-${i}`); + } + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcServer.toHttpEffect: concurrent streaming calls do not hang", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + + const N = 64; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + client.Count({ upto: 3 + (i % 3) }).pipe( + Stream.runCollect, + Effect.timeout("5 seconds"), + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 3, + }), + ), + { concurrency: N }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i]).toEqual( + Array.from({ length: 3 + (i % 3) }, (_, n) => n + 1), + ); + } + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +// === Durable Object pathway === +// These exercise the Worker's `*DO` handlers, which proxy through an +// `RpcClient` whose transport is `Cloudflare.toHttpClient(rpcDO.getByName(...))`. + +test( + "RpcServer.toHttpEffect Durable Object unary RPC response", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + const result = yield* client + .PingDO({ message: "hello-do" }) + .pipe(Effect.tapError(Console.log)); + expect(result.echo).toBe("hello-do"); + expect(result.n).toBeGreaterThan(0); + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcServer.toHttpEffect Durable Object streaming RPC response", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + const values = yield* client.CountDO({ upto: 5 }).pipe(Stream.runCollect); + expect(values).toEqual([1, 2, 3, 4, 5]); + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcServer.toHttpEffect Durable Object array payload streams response items in order", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + const messages = ["a", "b", "c", "d"]; + const values = yield* client.EchoDO({ messages }).pipe(Stream.runCollect); + expect(values).toEqual( + messages.map((message, index) => ({ index, message })), + ); + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "RpcServer.toHttpEffect Durable Object concurrent unary calls do not hang", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + + const N = 64; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + client.PingDO({ message: `m-${i}` }).pipe( + Effect.timeout("10 seconds"), + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 3, + }), + ), + { concurrency: 16 }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i].echo).toBe(`m-${i}`); + } + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 60_000 }, +); + +test( + "RpcServer.toHttpEffect Durable Object concurrent streaming calls do not hang", + Effect.gen(function* () { + const { url } = yield* stack; + + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(WorkerRpcs); + + const N = 32; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + client.CountDO({ upto: 3 + (i % 3) }).pipe( + Stream.runCollect, + Effect.timeout("10 seconds"), + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 3, + }), + ), + { concurrency: N }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i]).toEqual( + Array.from({ length: 3 + (i % 3) }, (_, n) => n + 1), + ); + } + }).pipe(Effect.scoped, Effect.provide(clientLayer(url))); + }).pipe(logLevel), + { timeout: 30_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcWorker.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcWorker.test.ts new file mode 100644 index 00000000000..a0aff0f3e9b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/RpcWorker.test.ts @@ -0,0 +1,121 @@ +import { expect } from "@effect/vitest"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Test from "alchemy/Test/Vitest"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as RpcClient from "effect/unstable/rpc/RpcClient"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import { CallerRpcs, TargetRpcs } from "./fixtures/rpc-worker-binding/group.ts"; +import Stack from "./fixtures/rpc-worker-binding/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +// Cap exponential backoff at 3s so retries stay bounded when the CF edge is +// slow (otherwise the geometric blow-up dominates wall time). +const readinessSchedule = Schedule.exponential("500 millis").pipe( + Schedule.either(Schedule.spaced("3 seconds")), +); + +// The caller worker forwards to the target via the service binding and wraps +// the call in `Effect.orDie` (see fixtures/caller-worker.ts). On a freshly +// deployed worker the service binding hasn't propagated to every Cloudflare +// edge yet, so the first calls fail with `Worker not found.` — which arrives +// at the client as a DEFECT, and `Effect.retry` does not retry defects. +// Promote defects to failures so the readiness retry can absorb the transient +// binding-propagation error (a genuine bug would simply keep failing until the +// retry budget is exhausted). +const retryReadyN = + (times: number) => + (eff: Effect.Effect) => + eff.pipe( + Effect.catchDefect((defect) => Effect.fail(defect)), + Effect.retry({ schedule: readinessSchedule, times }), + ); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +const clientLayer = (url: string) => + RpcClient.layerProtocolHttp({ url }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide( + Layer.succeed(RpcSerialization.RpcSerialization, RpcSerialization.ndjson), + ), + ); + +test( + "RpcWorker: target worker exposes Greet", + Effect.gen(function* () { + const { targetUrl } = yield* stack; + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(TargetRpcs); + const result = yield* client.Greet({ name: "world" }).pipe( + Effect.tapError(Console.log), + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 5, + }), + ); + expect(result.greeting).toBe("hello world"); + }).pipe(Effect.scoped, Effect.provide(clientLayer(targetUrl))); + }).pipe(logLevel), + { timeout: 60_000 }, +); + +test( + "RpcWorker.bind: caller proxies through service binding to target", + Effect.gen(function* () { + const { callerUrl } = yield* stack; + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(CallerRpcs); + const result = yield* client + .ProxyGreet({ name: "alchemy" }) + .pipe(Effect.tapError(Console.log), retryReadyN(10)); + expect(result.greeting).toBe("hello alchemy"); + }).pipe(Effect.scoped, Effect.provide(clientLayer(callerUrl))); + }).pipe(logLevel), + { timeout: 60_000 }, +); + +test( + "RpcWorker.bind: 100 concurrent ProxyGreet calls do not hang", + Effect.gen(function* () { + const { callerUrl } = yield* stack; + yield* Effect.gen(function* () { + const client = yield* RpcClient.make(CallerRpcs); + + // Warm the service binding by serially exercising it before the + // burst — workerd surfaces "Worker not found" defects until the + // target script is fully propagated to the same edge that the + // caller worker hits. + yield* client.ProxyGreet({ name: "warmup" }).pipe(retryReadyN(10)); + + const N = 100; + const results = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => i), + (i) => + client + .ProxyGreet({ name: `peer-${i}` }) + .pipe(Effect.timeout("10 seconds"), retryReadyN(10)), + { concurrency: 32 }, + ); + + expect(results).toHaveLength(N); + for (let i = 0; i < N; i++) { + expect(results[i].greeting).toBe(`hello peer-${i}`); + } + }).pipe(Effect.scoped, Effect.provide(clientLayer(callerUrl))); + }).pipe(logLevel), + { timeout: 60_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/TaggedDO.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/TaggedDO.test.ts new file mode 100644 index 00000000000..6c639a3125f --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/TaggedDO.test.ts @@ -0,0 +1,197 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"; +import Stack from "./fixtures/tagged-do/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +const testTimeout = 20_000; +const requestTimeout = "5 seconds"; +// Fresh `*.workers.dev` URLs propagate through the edge over a few seconds — +// the first requests routinely return 404 / 500 before the script is +// resolvable. `Effect.retry` only fires on Effect failures, not on HTTP +// status codes, so we explicitly `Effect.fail` non-2xx responses to force a +// retry through `readinessRetry`. +const readinessRetry = { + schedule: Schedule.exponential("500 millis"), + times: 15, +} as const; + +const requestUntilReady = ( + effect: Effect.Effect, +) => + effect.pipe( + Effect.timeout(requestTimeout), + Effect.flatMap( + Effect.fnUntraced(function* (res) { + return res.status >= 200 && res.status < 300 + ? res + : yield* Effect.fail( + new Error(`Worker not ready: ${res.status} ${yield* res.text}`), + ); + }), + ), + Effect.retry(readinessRetry), + ); + +// Each test addresses its own DO instance via a unique counter key so the +// tests are safe to run in parallel. The fixture Workers read this header +// and forward it as the argument to `counter.getByName(key)`. +const withCounterKey = (key: string) => + HttpClient.mapRequest(HttpClientRequest.setHeader("x-counter-key", key)); + +const reset = (url: string) => + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient; + yield* requestUntilReady(client.post(`${url}/reset`)); + }); + +test( + "D1 counter writes from WorkerA are visible from WorkerB (cross-script DO)", + Effect.gen(function* () { + const { urlA, urlB } = yield* stack; + const client = (yield* HttpClient.HttpClient).pipe( + withCounterKey("d1-cross"), + ); + + yield* reset(urlA).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + yield* reset(urlB).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + + const first = yield* requestUntilReady(client.post(`${urlA}/d1/increment`)); + expect(first.status).toBe(200); + expect((yield* first.json) as { value: number }).toEqual({ value: 1 }); + + const second = yield* requestUntilReady( + client.post(`${urlA}/d1/increment`), + ); + expect((yield* second.json) as { value: number }).toEqual({ value: 2 }); + + const fromB = yield* requestUntilReady(client.get(`${urlB}/d1`)); + expect(fromB.status).toBe(200); + expect((yield* fromB.json) as { value: number }).toEqual({ value: 2 }); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "DO storage counter writes from WorkerA are visible from WorkerB (cross-script DO)", + Effect.gen(function* () { + const { urlA, urlB } = yield* stack; + const client = (yield* HttpClient.HttpClient).pipe( + withCounterKey("do-cross"), + ); + + yield* reset(urlA).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + yield* reset(urlB).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + + const first = yield* requestUntilReady(client.post(`${urlA}/do/increment`)); + expect(first.status).toBe(200); + expect((yield* first.json) as { value: number }).toEqual({ value: 1 }); + + const second = yield* requestUntilReady( + client.post(`${urlA}/do/increment`), + ); + expect((yield* second.json) as { value: number }).toEqual({ value: 2 }); + + const fromB = yield* requestUntilReady(client.get(`${urlB}/do`)); + expect(fromB.status).toBe(200); + expect((yield* fromB.json) as { value: number }).toEqual({ value: 2 }); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "WorkerC hosts its own isolated Counter (writes from A/B are not visible from C)", + Effect.gen(function* () { + const { urlA, urlB, urlC } = yield* stack; + const client = (yield* HttpClient.HttpClient).pipe( + withCounterKey("isolation"), + ); + + yield* reset(urlA).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + yield* reset(urlB).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + yield* reset(urlC).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + + // Increment via WorkerA and WorkerB (both route to WorkerA's hosted Counter). + yield* requestUntilReady(client.post(`${urlA}/do/increment`)); + yield* requestUntilReady(client.post(`${urlB}/do/increment`)); + + const fromA = yield* requestUntilReady(client.get(`${urlA}/do`)); + expect((yield* fromA.json) as { value: number }).toEqual({ value: 2 }); + + // WorkerC hosts its own Counter namespace via `Counter.from(WorkerC)`, + // so its DO instance has never been written to. + const fromC = yield* requestUntilReady(client.get(`${urlC}/do`)); + expect((yield* fromC.json) as { value: number }).toEqual({ value: 0 }); + + // Writes through WorkerC do not leak back to WorkerA/B either. + yield* requestUntilReady(client.post(`${urlC}/do/increment`)); + yield* requestUntilReady(client.post(`${urlC}/do/increment`)); + yield* requestUntilReady(client.post(`${urlC}/do/increment`)); + + const cAfter = yield* requestUntilReady(client.get(`${urlC}/do`)); + expect((yield* cAfter.json) as { value: number }).toEqual({ value: 3 }); + + const aAfter = yield* requestUntilReady(client.get(`${urlA}/do`)); + expect((yield* aAfter.json) as { value: number }).toEqual({ value: 2 }); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "Writes from WorkerB are visible from WorkerA (bidirectional cross-script DO)", + Effect.gen(function* () { + const { urlA, urlB } = yield* stack; + const client = (yield* HttpClient.HttpClient).pipe( + withCounterKey("bidirectional"), + ); + + yield* reset(urlA).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + yield* reset(urlB).pipe( + Effect.provideService(HttpClient.HttpClient, client), + ); + + yield* requestUntilReady(client.post(`${urlB}/d1/increment`)); + yield* requestUntilReady(client.post(`${urlB}/d1/increment`)); + yield* requestUntilReady(client.post(`${urlB}/do/increment`)); + + const d1FromA = yield* requestUntilReady(client.get(`${urlA}/d1`)); + const doFromA = yield* requestUntilReady(client.get(`${urlA}/do`)); + + expect((yield* d1FromA.json) as { value: number }).toEqual({ value: 2 }); + expect((yield* doFromA.json) as { value: number }).toEqual({ value: 1 }); + }).pipe(logLevel), + { timeout: testTimeout }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/TaggedRpcDO.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/TaggedRpcDO.test.ts new file mode 100644 index 00000000000..4ad21a65037 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/TaggedRpcDO.test.ts @@ -0,0 +1,330 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import type * as Scope from "effect/Scope"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"; +import * as RpcClient from "effect/unstable/rpc/RpcClient"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import { CounterRpcs } from "./fixtures/tagged-rpc-do/group.ts"; +import Stack from "./fixtures/tagged-rpc-do/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const testTimeout = 30_000; +const requestTimeout = "5 seconds"; +// Fresh `*.workers.dev` URLs propagate through the edge over a few seconds — +// the first requests routinely return 404 / 500 before the script is +// resolvable. `Effect.retry` only fires on Effect failures, not on HTTP +// status codes, so we explicitly `Effect.fail` non-2xx responses to force a +// retry through `readinessRetry`. +// Cap exponential backoff at 3s so cold-start retries stay bounded when +// CF edge propagation is slow. +const readinessRetry = { + schedule: Schedule.exponential("500 millis").pipe( + Schedule.either(Schedule.spaced("3 seconds")), + ), + times: 15, +} as const; + +const requestUntilReady = ( + effect: Effect.Effect, +) => + effect.pipe( + Effect.timeout(requestTimeout), + Effect.flatMap( + Effect.fnUntraced(function* (res) { + return res.status >= 200 && res.status < 300 + ? res + : yield* Effect.fail( + new Error(`Worker not ready: ${res.status} ${yield* res.text}`), + ); + }), + ), + Effect.tapError(Effect.logError), + Effect.retry(readinessRetry), + ); + +// Each test addresses its own DO instance via a unique counter key so the +// tests are safe to run in parallel. WorkerB / WorkerC fixtures read +// the `x-counter-key` header; WorkerA's RPC takes `key` directly in +// every payload. +const withCounterKey = (key: string) => + HttpClient.mapRequest(HttpClientRequest.setHeader("x-counter-key", key)); + +// Build a typed `RpcClient` against WorkerA's URL. +// Every call rides through the same JSON edge as the worker.dev URL, +// so we share the readiness retry below at the test layer. +const rpcClientLayer = (url: string) => + RpcClient.layerProtocolHttp({ url }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide( + Layer.succeed(RpcSerialization.RpcSerialization, RpcSerialization.ndjson), + ), + ); + +// Drive a typed `RpcClient` body against WorkerA's URL. +// Each call gets its own scope (so the client is freed promptly). +// +// NOTE: `withRpcA` deliberately does NOT retry the body. The bodies below +// perform non-idempotent D1/DO increments, and a body-level retry would +// re-apply a mutation whose server-side write already committed but whose +// response failed transiently (the classic "expected 2 to be 1" flake). +// Readiness is instead handled idempotently: each test runs a retried +// `resetA`/`resetHttp` first (which also warms the edge), and the `beforeAll` +// gate below settles propagation before any test runs. +type RpcRequirements = + | RpcClient.Protocol + | RpcSerialization.RpcSerialization + | Scope.Scope; +const withRpcA = (url: string, body: Effect.Effect) => + body.pipe( + Effect.tapError((e) => Effect.logError("withRpcA error", e)), + Effect.scoped, + Effect.provide(rpcClientLayer(url)), + ) as Effect.Effect>; + +const resetHttp = (url: string, key: string) => + Effect.gen(function* () { + const client = (yield* HttpClient.HttpClient).pipe(withCounterKey(key)); + yield* requestUntilReady(client.post(`${url}/reset`)); + }); + +// `reset` is idempotent, so it's safe to retry — this doubles as the +// per-test readiness gate for WorkerA's RPC edge. +const resetA = (url: string, key: string) => + withRpcA( + url, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + yield* c.reset({ key }); + }), + ).pipe(Effect.retry(readinessRetry)); + +// Gate the deploy on all three workers' edges being resolvable (via the +// idempotent reset path), then let propagation settle, so the non-retried +// increment bodies below don't race cold-start. +const stack = beforeAll( + deploy(Stack).pipe( + Effect.tap(({ urlA, urlB, urlC }) => + Effect.all( + [ + resetA(urlA, "warmup"), + resetHttp(urlB, "warmup"), + resetHttp(urlC, "warmup"), + ], + { concurrency: "unbounded" }, + ), + ), + // just give it some extra time to propagate + Effect.tap(Effect.sleep("5 seconds")), + ), +); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +test( + "RpcWorker WorkerA exposes the same RPC surface as the underlying DO", + Effect.gen(function* () { + const { urlA, urlB, urlC } = yield* stack; + console.log("URLS:", { urlA, urlB, urlC }); + const key = "rpc-worker-a"; + + yield* resetA(urlA, key); + + yield* withRpcA( + urlA, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + const first = yield* c.incrementD1({ key }); + console.log("withRpcA", first); + const second = yield* c.incrementD1({ key }); + const get = yield* c.getD1({ key }); + expect(first.value).toBe(1); + expect(second.value).toBe(2); + expect(get.value).toBe(2); + }), + ); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "D1 counter writes via WorkerA's RPC are visible from WorkerB (cross-script DO)", + Effect.gen(function* () { + const { urlA, urlB } = yield* stack; + const key = "d1-cross"; + + yield* resetA(urlA, key); + yield* resetHttp(urlB, key); + + yield* withRpcA( + urlA, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + const inc1 = yield* c.incrementD1({ key }); + expect(inc1.value).toBe(1); + const inc2 = yield* c.incrementD1({ key }); + expect(inc2.value).toBe(2); + }), + ); + + const httpClient = (yield* HttpClient.HttpClient).pipe(withCounterKey(key)); + const fromB = yield* httpClient + .get(`${urlB}/d1`) + .pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + expect(fromB.status).toBe(200); + expect((yield* fromB.json) as { value: number }).toEqual({ value: 2 }); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "DO storage counter writes via WorkerA's RPC are visible from WorkerB (cross-script DO)", + Effect.gen(function* () { + const { urlA, urlB } = yield* stack; + const key = "do-cross"; + + yield* resetA(urlA, key); + yield* resetHttp(urlB, key); + + yield* withRpcA( + urlA, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + const inc1 = yield* c.incrementDO({ key }); + expect(inc1.value).toBe(1); + const inc2 = yield* c.incrementDO({ key }); + expect(inc2.value).toBe(2); + }), + ); + + const httpClient = (yield* HttpClient.HttpClient).pipe(withCounterKey(key)); + const fromB = yield* httpClient + .get(`${urlB}/do`) + .pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + expect(fromB.status).toBe(200); + expect((yield* fromB.json) as { value: number }).toEqual({ value: 2 }); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "Writes from WorkerB are visible from WorkerA's RPC (bidirectional cross-script DO)", + Effect.gen(function* () { + const { urlA, urlB } = yield* stack; + const key = "bidirectional"; + + yield* resetA(urlA, key); + yield* resetHttp(urlB, key); + + const httpClient = (yield* HttpClient.HttpClient).pipe(withCounterKey(key)); + // These increments are non-idempotent: retrying a request whose write + // committed but whose response failed would over-count. The `resetHttp` + // above (retried) has already warmed WorkerB's edge, so run them once. + yield* httpClient + .post(`${urlB}/d1/increment`) + .pipe(Effect.timeout(requestTimeout)); + yield* httpClient + .post(`${urlB}/d1/increment`) + .pipe(Effect.timeout(requestTimeout)); + yield* httpClient + .post(`${urlB}/do/increment`) + .pipe(Effect.timeout(requestTimeout)); + + yield* withRpcA( + urlA, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + const d1 = yield* c.getD1({ key }); + const dox = yield* c.getDO({ key }); + expect(d1.value).toBe(2); + expect(dox.value).toBe(1); + }), + ); + }).pipe(logLevel), + { timeout: testTimeout }, +); + +test( + "WorkerC hosts its own isolated Counter (writes from A/B are not visible from C)", + Effect.gen(function* () { + const { urlA, urlB, urlC } = yield* stack; + const key = "isolation"; + + yield* resetA(urlA, key); + yield* resetHttp(urlB, key); + yield* resetHttp(urlC, key); + + // Increment via WorkerA (RPC) and WorkerB (HTTP → cross-script DO); + // both route to WorkerA's hosted Counter. + yield* withRpcA( + urlA, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + yield* c.incrementDO({ key }); + }), + ); + + const httpClient = (yield* HttpClient.HttpClient).pipe(withCounterKey(key)); + // Non-idempotent increment — run once (resetHttp above warmed the edge). + yield* httpClient + .post(`${urlB}/do/increment`) + .pipe(Effect.timeout(requestTimeout)); + + // WorkerA sees value 2 (its own + WorkerB's cross-script). + yield* withRpcA( + urlA, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + const fromA = yield* c.getDO({ key }); + expect(fromA.value).toBe(2); + }), + ); + + // WorkerC hosts its own Counter namespace via `Counter.from(WorkerC)`, + // so its DO instance has never been written to. + const fromC = yield* httpClient + .get(`${urlC}/do`) + .pipe(Effect.timeout(requestTimeout), Effect.retry(readinessRetry)); + expect((yield* fromC.json) as { value: number }).toEqual({ value: 0 }); + + // Writes through WorkerC do not leak back to WorkerA either. + yield* httpClient + .post(`${urlC}/do/increment`) + .pipe(Effect.timeout(requestTimeout)); + yield* httpClient + .post(`${urlC}/do/increment`) + .pipe(Effect.timeout(requestTimeout)); + yield* httpClient + .post(`${urlC}/do/increment`) + .pipe(Effect.timeout(requestTimeout)); + + const cAfter = yield* httpClient + .get(`${urlC}/do`) + .pipe(Effect.timeout(requestTimeout)); + expect((yield* cAfter.json) as { value: number }).toEqual({ value: 3 }); + + yield* withRpcA( + urlA, + Effect.gen(function* () { + const c = yield* RpcClient.make(CounterRpcs); + const aAfter = yield* c.getDO({ key }); + expect(aAfter.value).toBe(2); + }), + ); + }).pipe(logLevel), + { timeout: testTimeout }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Worker.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Worker.test.ts new file mode 100644 index 00000000000..0d73d49ac00 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Worker.test.ts @@ -0,0 +1,863 @@ +import { adopt } from "@/AdoptPolicy"; +import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment"; +import * as Cloudflare from "@/Cloudflare/index.ts"; +import * as R2 from "@/Cloudflare/R2"; +import { Stack } from "@/Stack"; +import { State } from "@/State"; +import * as Test from "@/Test/Vitest"; +import * as workers from "@distilled.cloud/cloudflare/workers"; +import { describe, expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import { MinimumLogLevel } from "effect/References"; +import * as pathe from "pathe"; +import { cloneFixture } from "../Utils/Fixture.ts"; +import { expectUrlContains } from "../Utils/Http.ts"; +import { + expectWorkerExists, + expectWorkersDevPreviews, + expectWorkersDevSubdomain, + findWorker, + getWorkerTags, + waitForWorkerToBeDeleted, +} from "../Utils/Worker.ts"; +import InternalWorker from "./fixtures/internal-worker.ts"; + +const { test } = Test.make({ providers: Cloudflare.providers() }); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const main = pathe.resolve(import.meta.dirname, "fixtures/worker.ts"); + +describe.concurrent("Cloudflare.Worker", () => { + test.provider("create, update, delete worker", (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const s = yield* Stack; + + yield* stack.destroy(); + + const worker = yield* stack.deploy( + Effect.gen(function* () { + yield* R2.R2Bucket("Bucket", { + storageClass: "Standard", + }); + + const worker = yield* Cloudflare.Worker("TestWorker", { + main, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { + date: "2024-01-01", + }, + }); + + return worker; + }), + ); + + const actualWorker = yield* findWorker(worker.workerName, accountId); + expect(actualWorker?.scriptName).toEqual(worker.workerName); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + `alchemy:stack:${s.name}`, + ); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + `alchemy:stage:${s.stage}`, + ); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + "alchemy:id:TestWorker", + ); + + // Verify the workers.dev subdomain is enabled on Cloudflare + // (rather than just trusting the resource's output attributes). + expect(worker.url).toBeDefined(); + const initialSubdomain = yield* workers.getScriptSubdomain({ + accountId, + scriptName: worker.workerName, + }); + expect(initialSubdomain).toEqual({ + enabled: true, + previewsEnabled: true, + }); + + // Update the worker + const updatedWorker = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("TestWorker", { + main, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { + date: "2024-01-01", + }, + }); + }), + ); + + const actualUpdatedWorker = yield* findWorker( + updatedWorker.workerName, + accountId, + ); + expect(actualUpdatedWorker?.scriptName).toEqual(updatedWorker.workerName); + const actualUpdatedSubdomain = yield* workers.getScriptSubdomain({ + accountId, + scriptName: updatedWorker.workerName, + }); + expect(actualUpdatedSubdomain).toEqual({ + enabled: true, + previewsEnabled: true, + }); + + yield* stack.destroy(); + + yield* waitForWorkerToBeDeleted(worker.workerName, accountId); + }).pipe(logLevel), + ); + + test.provider("create, update, delete worker with assets", (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const s = yield* Stack; + + yield* stack.destroy(); + + const worker = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("TestWorkerWithAssets", { + main, + assets: pathe.resolve(import.meta.dirname, "assets"), + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { + date: "2024-01-01", + }, + }); + }), + ); + + const actualWorker = yield* findWorker(worker.workerName, accountId); + expect(actualWorker?.scriptName).toEqual(worker.workerName); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + `alchemy:stack:${s.name}`, + ); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + `alchemy:stage:${s.stage}`, + ); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + "alchemy:id:TestWorkerWithAssets", + ); + + // Verify the worker has assets + expect(worker.hash?.assets).toBeDefined(); + + // Verify the workers.dev subdomain is enabled on Cloudflare + // (rather than just trusting the resource's output attributes). + expect(worker.url).toBeDefined(); + const assetsWorkerSubdomain = yield* workers.getScriptSubdomain({ + accountId, + scriptName: worker.workerName, + }); + expect(assetsWorkerSubdomain).toEqual({ + enabled: true, + previewsEnabled: true, + }); + + // Update the worker + const updatedWorker = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("TestWorkerWithAssets", { + main, + assets: pathe.resolve(import.meta.dirname, "assets"), + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { + date: "2024-01-01", + }, + }); + }), + ); + + const actualUpdatedWorker = yield* findWorker( + updatedWorker.workerName, + accountId, + ); + expect(actualUpdatedWorker?.scriptName).toEqual(updatedWorker.workerName); + expect(updatedWorker.hash?.assets).toBeDefined(); + + // Final update + const finalWorker = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("TestWorkerWithAssets", { + main, + url: true, + assets: pathe.resolve(import.meta.dirname, "assets"), + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { + date: "2024-01-01", + }, + }); + }), + ); + + yield* stack.destroy(); + + yield* waitForWorkerToBeDeleted(finalWorker.workerName, accountId); + }).pipe(logLevel), + ); + + // ───────────────────────────────────────────────────────────────────── + // Asset hashing & keepAssets behavior + // + // `hash.assets` is content-addressed: it must depend only on the bytes + // in the directory, not on where the directory lives. The provider + // uses that hash to decide whether to upload a fresh manifest or tell + // Cloudflare to keep the existing one (`keepAssets: true`). These + // tests pin down the user-visible contract: + // + // 1. Same bytes at a different path → same hash, no re-upload. + // 2. Different bytes (any change) → new hash, re-upload. + // 3. A worker-only change leaves the asset hash alone, so the + // script update goes out without re-walking the asset tree. + // + // The "moved path" cases also guard against the regression where state + // written by one machine (e.g. a CI runner) recorded an absolute path + // that the next machine couldn't open — the deploy used to crash with + // `NotFound: FileSystem.readDirectory`. + // ───────────────────────────────────────────────────────────────────── + + const assetsFixtureDir = pathe.resolve(import.meta.dirname, "assets"); + + test.provider( + "Worker assets: relocating to a fresh path with identical bytes preserves hash and keeps assets", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const fs = yield* FileSystem.FileSystem; + + yield* stack.destroy(); + + const dirA = yield* cloneFixture(assetsFixtureDir, { + prefix: "alchemy-worker-assets-a-", + }); + const dirB = yield* cloneFixture(assetsFixtureDir, { + prefix: "alchemy-worker-assets-b-", + }); + + const deploy = (assetsDir: string) => + stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("RelocatedAssets", { + main, + assets: assetsDir, + url: true, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + const v1 = yield* deploy(dirA); + expect(v1.hash?.assets).toBeDefined(); + yield* expectWorkerExists(v1.workerName, accountId); + yield* expectUrlContains(`${v1.url!}/index.html`, "Hello from Worker", { + timeout: "120 seconds", + label: "v1 served", + }); + + // Wipe dirA before the second deploy. If anything in the apply + // path still tries to read the previously-recorded directory, + // this is where we'd fail with NotFound. + yield* fs.remove(dirA, { recursive: true }); + + const v2 = yield* deploy(dirB); + + // Identical bytes ⇒ identical asset hash ⇒ keepAssets path. + expect(v2.hash?.assets).toEqual(v1.hash?.assets); + // The script binding stayed live; the URL keeps serving. + yield* expectUrlContains(`${v2.url!}/index.html`, "Hello from Worker", { + timeout: "60 seconds", + label: "v2 served", + }); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(v1.workerName, accountId); + }).pipe(logLevel), + { timeout: 360_000 }, + ); + + test.provider( + "Worker assets: editing a file changes the hash and republishes the manifest", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + + yield* stack.destroy(); + + const dir = yield* cloneFixture(assetsFixtureDir, { + prefix: "alchemy-worker-assets-edit-", + }); + const indexPath = path.join(dir, "index.html"); + + const v1Marker = `worker-assets-v1-${Date.now()}`; + yield* fs.writeFileString( + indexPath, + `${v1Marker}${v1Marker}`, + ); + + const deploy = () => + stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("EditedAssets", { + main, + assets: dir, + url: true, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + const v1 = yield* deploy(); + expect(v1.hash?.assets).toBeDefined(); + yield* expectUrlContains(`${v1.url!}/index.html`, v1Marker, { + timeout: "120 seconds", + label: "v1 marker", + }); + + const v2Marker = `worker-assets-v2-${Date.now()}`; + yield* fs.writeFileString( + indexPath, + `${v2Marker}${v2Marker}`, + ); + + const v2 = yield* deploy(); + expect(v2.hash?.assets).toBeDefined(); + expect(v2.hash?.assets).not.toEqual(v1.hash?.assets); + yield* expectUrlContains(`${v2.url!}/index.html`, v2Marker, { + timeout: "60 seconds", + label: "v2 marker", + }); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(v1.workerName, accountId); + }).pipe(logLevel), + { timeout: 360_000 }, + ); + + test.provider( + "Worker assets: a bundle-only change keeps the asset manifest (hash.assets stable)", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + + yield* stack.destroy(); + + const dir = yield* cloneFixture(assetsFixtureDir, { + prefix: "alchemy-worker-assets-bundle-only-", + }); + // Write the worker entry into a fresh temp dir so we can edit + // it between deploys to force a bundle hash change without + // touching the assets directory. + const workerDir = yield* fs.makeTempDirectory({ + prefix: "alchemy-worker-assets-bundle-only-entry-", + }); + const workerPath = path.join(workerDir, "worker.ts"); + const writeWorker = (marker: string) => + fs.writeFileString( + workerPath, + `export default { + fetch: async () => new Response(${JSON.stringify(`Hello from BundleOnly: ${marker}`)}), + }; + `, + ); + + const deploy = () => + stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("BundleOnlyChange", { + main: workerPath, + assets: dir, + url: true, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + yield* writeWorker("v1"); + const v1 = yield* deploy(); + expect(v1.hash?.assets).toBeDefined(); + expect(v1.hash?.bundle).toBeDefined(); + + yield* writeWorker("v2"); + const v2 = yield* deploy(); + // Bundle changed (worker source edited) → hash.bundle moves. + // Assets are byte-identical → hash.assets must not move, and + // the keepAssets branch must keep the manifest live. + expect(v2.hash?.bundle).not.toEqual(v1.hash?.bundle); + expect(v2.hash?.assets).toEqual(v1.hash?.assets); + yield* expectUrlContains(`${v2.url!}/index.html`, "Hello from Worker", { + timeout: "60 seconds", + label: "assets still served after bundle-only change", + }); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(v1.workerName, accountId); + }).pipe(logLevel), + { timeout: 360_000 }, + ); + + test.provider("create, update, delete internal worker", (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const s = yield* Stack; + + yield* stack.destroy(); + + const worker = yield* stack.deploy( + Effect.gen(function* () { + return yield* InternalWorker; + }), + ); + + const actualWorker = yield* findWorker(worker.workerName, accountId); + expect(actualWorker?.scriptName).toEqual(worker.workerName); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + `alchemy:stack:${s.name}`, + ); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + `alchemy:stage:${s.stage}`, + ); + expect(yield* getWorkerTags(worker.workerName, accountId)).toContain( + "alchemy:id:InternalWorker", + ); + + const updatedWorker = yield* stack.deploy( + Effect.gen(function* () { + return yield* InternalWorker; + }), + ); + + expect(updatedWorker.workerName).toEqual(worker.workerName); + + yield* stack.destroy(); + + yield* waitForWorkerToBeDeleted(worker.workerName, accountId); + }).pipe(logLevel), + ); + + // ── Engine-level adoption ───────────────────────────────────────────────── + // + // The engine always calls `provider.read` when there is no prior state, and + // routes on the returned shape: + // + // - undefined → resource doesn't exist, drive a normal create + // - plain attrs → resource exists and is owned by us (Worker + // determines this from `alchemy:*` tags); silent + // adoption regardless of `--adopt` + // - `Unowned(attrs)` → resource exists but the tags don't identify us; + // the engine fails with `OwnedBySomeoneElse` unless + // the user opted in via `adopt(true)` / `--adopt`, + // in which case it's a silent takeover. + // + // The tests below use `test.provider`'s scratch state so we can wipe state + // mid-test while leaving the actual Cloudflare Worker in place — simulating + // "the user created/deployed this worker before, but this state store has + // never seen it" (e.g. CLI-driven first deploy on a fresh machine, or a + // state-store reset). + + test.provider( + "owned worker (matching alchemy tags) is silently adopted without --adopt", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + + yield* stack.destroy(); + + // Use a fixed physical name so the worker's identity persists + // across a state-store wipe (otherwise `createWorkerName` would + // pick a fresh random suffix on the second deploy and we'd just + // be creating a new worker, not adopting). + const physicalName = `alchemy-test-owned-adopt-${Math.random() + .toString(36) + .slice(2, 8)}`; + + // Phase 1: deploy normally so a real Worker exists on Cloudflare, + // tagged with this stack/stage/id. + const initial = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("AdoptableWorker", { + main, + name: physicalName, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + expect(initial.workerName).toEqual(physicalName); + expect(yield* findWorker(physicalName, accountId)).toBeDefined(); + + // Phase 2: wipe local state for this resource — the worker stays on + // Cloudflare. From the next deploy's perspective this looks like a + // fresh state store that has never seen this resource. + yield* Effect.gen(function* () { + const state = yield* yield* State; + yield* state.delete({ + stack: stack.name, + stage: "test", + fqn: "AdoptableWorker", + }); + }).pipe(Effect.provide(stack.state)); + + // Phase 3: redeploy *without* `adopt(true)`. The engine calls + // `provider.read`, the Worker's read sees its own alchemy tags and + // returns plain (owned) attrs, and the engine silently adopts. + // No `--adopt` flag is required. + const adopted = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("AdoptableWorker", { + main, + name: physicalName, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + expect(adopted.workerName).toEqual(physicalName); + + const persisted = yield* Effect.gen(function* () { + const state = yield* yield* State; + return yield* state.get({ + stack: stack.name, + stage: "test", + fqn: "AdoptableWorker", + }); + }).pipe(Effect.provide(stack.state)); + + expect(persisted?.status).toBeDefined(); + expect((persisted as any)?.attr).toMatchObject({ + workerName: physicalName, + }); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(physicalName, accountId); + }).pipe(logLevel), + ); + + test.provider("adopt(true) takes over a foreign-tagged worker", (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + + yield* stack.destroy(); + + // Phase 1: deploy under logical id "Original" with an explicit + // physical name. The Cloudflare Worker is now tagged + // `alchemy:id:Original` — i.e. owned by *that* logical resource. + const physicalName = `alchemy-test-adopt-takeover-${Math.random() + .toString(36) + .slice(2, 8)}`; + const original = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("Original", { + main, + name: physicalName, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + expect(yield* findWorker(original.workerName, accountId)).toBeDefined(); + expect(yield* getWorkerTags(physicalName, accountId)).toContain( + "alchemy:id:Original", + ); + + // Wipe state for the "Original" entry; the worker stays on Cloudflare. + yield* Effect.gen(function* () { + const state = yield* yield* State; + yield* state.delete({ + stack: stack.name, + stage: "test", + fqn: "Original", + }); + }).pipe(Effect.provide(stack.state)); + + // Phase 2: redeploy under a *different* logical id with the same + // physical name and `adopt(true)`. `Worker.read` returns + // `Unowned(attrs)` because the existing tags identify a different + // logical id; with adopt enabled the engine takes over and the + // follow-up create/update rewrites the tags. (The rejection path + // — same scenario without `adopt(true)` — is covered by the unit + // tests in `plan.test.ts`.) + const takenOver = yield* stack + .deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("Different", { + main, + name: physicalName, + subdomain: { enabled: true, previewsEnabled: true }, + compatibility: { date: "2024-01-01" }, + }); + }), + ) + .pipe(adopt(true)); + + expect(takenOver.workerName).toEqual(physicalName); + + const newTags = yield* getWorkerTags(physicalName, accountId); + expect(newTags).toContain("alchemy:id:Different"); + expect(newTags).not.toContain("alchemy:id:Original"); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(physicalName, accountId); + }).pipe(logLevel), + ); + + // First-deploy behaviour: the default (omitting `url`) must enable + // the workers.dev subdomain, and `url: false` must disable it. Both + // are asserted against live Cloudflare state via `getScriptSubdomain`, + // not just the resource's output attributes. + test.provider( + "url defaults to enabling the workers.dev subdomain on first deploy", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + + yield* stack.destroy(); + + const worker = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("SubdomainDefaultWorker", { + main, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + expect(worker.url).toBeDefined(); + yield* expectWorkersDevSubdomain(worker.workerName, accountId, true); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(worker.workerName, accountId); + }).pipe(logLevel), + ); + + test.provider( + "url: false disables the workers.dev subdomain on first deploy", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + + yield* stack.destroy(); + + const worker = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("SubdomainDisabledWorker", { + main, + url: false, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + expect(worker.url).toBeUndefined(); + yield* expectWorkersDevSubdomain(worker.workerName, accountId, false); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(worker.workerName, accountId); + }).pipe(logLevel), + ); + + // Update regression: toggling `url` between deploys must propagate + // to the live Cloudflare subdomain state. Before this regression + // was fixed, the reconciler diffed `news.url !== olds.url` and + // drove the API call symmetrically — but the new observed-vs- + // desired check inside reconcile must still flip the toggle when + // props really do change. + test.provider( + "toggling url between deploys flips the workers.dev subdomain", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + + yield* stack.destroy(); + + const deploy = (url: boolean) => + stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("SubdomainToggleWorker", { + main, + url, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + const v1 = yield* deploy(true); + expect(v1.url).toBeDefined(); + yield* expectWorkersDevSubdomain(v1.workerName, accountId, true); + + const v2 = yield* deploy(false); + expect(v2.workerName).toEqual(v1.workerName); + expect(v2.url).toBeUndefined(); + yield* expectWorkersDevSubdomain(v2.workerName, accountId, false); + + const v3 = yield* deploy(true); + expect(v3.workerName).toEqual(v1.workerName); + expect(v3.url).toBeDefined(); + yield* expectWorkersDevSubdomain(v3.workerName, accountId, true); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(v1.workerName, accountId); + }).pipe(logLevel), + ); + + // Drift regression: if something external (a previous failed deploy, + // a Cloudflare dashboard toggle, the bootstrap path in `loginWithCloudflare`) + // leaves the workers.dev subdomain in `enabled: true, previewsEnabled: false`, + // a redeploy must observe `previewsEnabled` and flip it back on. The + // pre-fix reconciler diffed only `enabled` against desired, so it + // skipped the API call and let the drift persist. + test.provider( + "redeploy re-enables previewsEnabled when externally disabled", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + + yield* stack.destroy(); + + // Deploy with different compatibility dates to force the update. + const deploy = (date: string) => + stack.deploy( + Cloudflare.Worker("SubdomainPreviewsDriftWorker", { + main, + compatibility: { date }, + }), + ); + + const v1 = yield* deploy("2026-01-01"); + yield* expectWorkersDevPreviews(v1.workerName, accountId, { + enabled: true, + previewsEnabled: true, + }); + + // Simulate external drift: leave `enabled: true` but turn + // `previewsEnabled` off out-of-band. + yield* workers.createScriptSubdomain({ + accountId, + scriptName: v1.workerName, + enabled: true, + previewsEnabled: false, + }); + const drifted = yield* workers.getScriptSubdomain({ + accountId, + scriptName: v1.workerName, + }); + expect(drifted).toEqual({ enabled: true, previewsEnabled: false }); + + const v2 = yield* deploy("2026-01-02"); + expect(v2.workerName).toEqual(v1.workerName); + yield* expectWorkersDevPreviews(v2.workerName, accountId, { + enabled: true, + previewsEnabled: true, + }); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(v1.workerName, accountId); + }).pipe(logLevel), + ); + + // `domains` should reflect the workers.dev URL when the subdomain is + // enabled and be empty when it isn't. `worker.url` is just `domains[0]`, + // so the two must stay in lockstep across deploys. + test.provider( + "domains reflects the workers.dev subdomain and tracks url", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + + yield* stack.destroy(); + + const deploy = (url: boolean) => + stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("DomainsWorker", { + main, + url, + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + const enabled = yield* deploy(true); + expect(enabled.domains).toHaveLength(1); + expect(enabled.domains[0]).toMatch(/\.workers\.dev$/); + expect(enabled.url).toEqual(enabled.domains[0]); + + const disabled = yield* deploy(false); + expect(disabled.domains).toEqual([]); + expect(disabled.url).toBeUndefined(); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(enabled.workerName, accountId); + }).pipe(logLevel), + ); + + // When custom domains are attached, they come first in `domains` (in + // the order the user provided them), followed by the workers.dev URL + // when the subdomain is enabled. `worker.url` is `domains[0]`, so the + // custom domain wins. + const customDomainZone = process.env.CLOUDFLARE_TEST_WORKER_DOMAIN_ZONE_NAME; + test.provider.skipIf(!customDomainZone)( + "domains puts custom domains before workers.dev and url is the first", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const suffix = process.env.PULL_REQUEST ?? process.env.USER ?? "local"; + const domainA = `alchemy-worker-a-${suffix}.${customDomainZone}`; + const domainB = `alchemy-worker-b-${suffix}.${customDomainZone}`; + + yield* stack.destroy(); + + const worker = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("CustomDomainWorker", { + main, + domain: [domainA, domainB], + compatibility: { date: "2024-01-01" }, + }); + }), + ); + + expect(worker.domains.slice(0, 2)).toEqual([ + `https://${domainA}`, + `https://${domainB}`, + ]); + expect(worker.domains[2]).toMatch(/\.workers\.dev$/); + expect(worker.url).toEqual(`https://${domainA}`); + + // Reorder — `domains[0]` should follow. + const swapped = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Worker("CustomDomainWorker", { + main, + domain: [domainB, domainA], + compatibility: { date: "2024-01-01" }, + }); + }), + ); + expect(swapped.domains.slice(0, 2)).toEqual([ + `https://${domainB}`, + `https://${domainA}`, + ]); + expect(swapped.url).toEqual(`https://${domainB}`); + + yield* stack.destroy(); + yield* waitForWorkerToBeDeleted(worker.workerName, accountId); + }).pipe(logLevel), + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/WorkerBinding.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/WorkerBinding.test.ts new file mode 100644 index 00000000000..6ac528137e1 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/WorkerBinding.test.ts @@ -0,0 +1,71 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import Stack from "./fixtures/worker-worker-binding/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +// Cold-start retry — fresh `workers.dev` URLs take a few seconds to start +// answering 200, so the very first request rides this schedule. +const coldStartRetry = Effect.retry({ + schedule: Schedule.exponential("500 millis").pipe( + Schedule.both(Schedule.recurs(20)), + ), +}); + +test( + "target worker's own fetch handler responds", + Effect.gen(function* () { + const { targetUrl } = yield* stack; + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + + const res = yield* client.get(targetUrl).pipe(coldStartRetry); + expect(res.status).toBe(200); + expect(yield* res.text).toBe("hello from BindingTargetWorker"); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "async caller can call target's RPC method via service binding", + Effect.gen(function* () { + const { asyncCallerUrl } = yield* stack; + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + + const res = yield* client + .get(`${asyncCallerUrl}/?name=alice`) + .pipe(coldStartRetry); + expect(res.status).toBe(200); + expect(yield* res.text).toBe("hello alice"); + }).pipe(logLevel), + { timeout: 30_000 }, +); + +test( + "effect caller can call target's RPC method via bindWorker", + Effect.gen(function* () { + const { effectCallerUrl } = yield* stack; + const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient); + + const res = yield* client + .get(`${effectCallerUrl}/?name=bob`) + .pipe(coldStartRetry); + expect(res.status).toBe(200); + expect(yield* res.text).toBe("hello bob"); + }).pipe(logLevel), + { timeout: 30_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/WorkerEnv.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/WorkerEnv.test.ts new file mode 100644 index 00000000000..453fa5c57b0 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/WorkerEnv.test.ts @@ -0,0 +1,100 @@ +import * as Cloudflare from "@/Cloudflare/index.ts"; +import * as Test from "@/Test/Vitest"; +import { describe, expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import { expectUrlContains } from "../Utils/Http.ts"; +import Stack from "./fixtures/env/stack.ts"; + +// Populate process.env before deploy so the worker fixture's +// `Config.xxx(...)` reads resolve at deploy time (default provider). +const CONFIG_STR_VALUE = (process.env.CONFIG_STR = "config-string-value"); +const CONFIG_NUM_VALUE = (process.env.CONFIG_NUM = "1234"); +const CONFIG_REDACTED_VALUE = (process.env.CONFIG_REDACTED = + "config-redacted-value"); +const CONFIG_REDACTED_INIT_VALUE = (process.env.CONFIG_REDACTED_INIT = + "config-redacted-init-value"); + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const stack = beforeAll(deploy(Stack)); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +describe.concurrent("Cloudflare.Worker env bindings", () => { + test( + "async worker round-trips every supported binding shape", + Effect.gen(function* () { + const { asyncUrl } = yield* stack; + expect(asyncUrl).toBeTypeOf("string"); + console.log(asyncUrl); + + const body = yield* expectUrlContains(asyncUrl, '"STR":"hello"', { + timeout: "60 seconds", + label: "async env-worker response", + }); + expect(JSON.parse(body)).toEqual({ + STR: "hello", + NUM: 42, + BOOL: true, + NULL: null, + OBJ: { nested: { value: "ok" }, count: 7 }, + ARR: [1, 2, 3], + SECRET_STR: "shh", + SECRET_JSON: { token: "abc", scopes: ["read", "write"] }, + CONFIG_STR: CONFIG_STR_VALUE, + CONFIG_NUM: Number(CONFIG_NUM_VALUE), + CONFIG_REDACTED: CONFIG_REDACTED_VALUE, + }); + }).pipe(logLevel), + ); + + test( + "effect worker round-trips env: literals and Redacted via WorkerEnvironment", + Effect.gen(function* () { + const { effectUrl } = yield* stack; + + const body = yield* expectUrlContains( + `${effectUrl}/env`, + '"STR":"hello"', + { timeout: "60 seconds", label: "effect env-worker /env" }, + ); + expect(JSON.parse(body)).toEqual({ + STR: "hello", + NUM: 42, + BOOL: true, + NULL: null, + OBJ: { nested: { value: "ok" }, count: 7 }, + ARR: [1, 2, 3], + SECRET_STR: "shh", + SECRET_JSON: { token: "abc", scopes: ["read", "write"] }, + }); + }).pipe(logLevel), + ); + + test( + "effect worker round-trips Config.xxx bindings captured in Init", + Effect.gen(function* () { + const { effectUrl } = yield* stack; + + const body = yield* expectUrlContains( + `${effectUrl}/config`, + '"CONFIG_STR"', + { timeout: "60 seconds", label: "effect env-worker /config" }, + ); + expect(JSON.parse(body)).toEqual({ + CONFIG_STR: CONFIG_STR_VALUE, + CONFIG_NUM: Number(CONFIG_NUM_VALUE), + CONFIG_REDACTED: CONFIG_REDACTED_VALUE, + CONFIG_REDACTED_INIT: CONFIG_REDACTED_INIT_VALUE, + CONFIG_REDACTED_INIT_IS_REDACTED: true, + }); + }).pipe(logLevel), + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Workflow.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Workflow.test.ts new file mode 100644 index 00000000000..2d354d7a231 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/Workflow.test.ts @@ -0,0 +1,103 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import Stack from "./fixtures/workflow/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const stack = beforeAll( + deploy(Stack).pipe( + // Let the freshly-deployed worker (and its Workflow binding) settle before + // the first run so a step doesn't error mid-propagation. + Effect.tap(Effect.sleep("5 seconds")), + ), +); +afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); + +interface WorkflowStatus { + status: string; + output?: { greeting: string; envBindingCount: number }; + error?: { message?: string } | null; +} + +// Start a fresh workflow instance and poll until it reaches a terminal state. +// A transient `errored` during edge/binding propagation fails this effect so +// the caller can retry with a brand-new instance. +const runWorkflowToCompletion = (url: string) => + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient; + + // Cloudflare's edge takes a few seconds to start serving a fresh + // workers.dev URL, so retry until it returns 200 (a fresh URL also + // returns 404 transiently, which is not an HTTP error so Effect.retry + // does not catch it unless we explicitly fail on non-200). + const startRes = yield* client.post(`${url}/workflow/start/world`).pipe( + Effect.flatMap((res) => + res.status === 200 + ? Effect.succeed(res) + : Effect.fail(new Error(`Worker not ready: ${res.status}`)), + ), + Effect.retry({ + schedule: Schedule.exponential("500 millis"), + times: 15, + }), + ); + const { instanceId } = (yield* startRes.json) as { instanceId: string }; + expect(instanceId).toBeTypeOf("string"); + + const lastStatus = yield* client + .get(`${url}/workflow/status/${instanceId}`) + .pipe( + Effect.flatMap((res) => res.json), + Effect.map((json) => json as unknown as WorkflowStatus), + Effect.repeat({ + schedule: Schedule.spaced("2 seconds"), + until: (s) => s.status === "complete" || s.status === "errored", + times: 12, + }), + ); + + // Surface a non-complete terminal state as a failure so the outer retry + // can take another swing (a fresh worker occasionally errors a step while + // its bindings are still propagating). + if (lastStatus.status !== "complete") { + return yield* Effect.fail( + new Error( + `workflow ${lastStatus.status}: ${JSON.stringify(lastStatus.error)}`, + ), + ); + } + return lastStatus; + }); + +test( + "deployed worker can run a workflow to completion", + Effect.gen(function* () { + const out = yield* stack; + const url = out.url; + expect(url).toBeTypeOf("string"); + + const lastStatus = yield* runWorkflowToCompletion(url).pipe( + Effect.retry({ schedule: Schedule.spaced("3 seconds"), times: 2 }), + ); + + expect(lastStatus.status).toBe("complete"); + expect(lastStatus.error).toBeFalsy(); + expect(lastStatus.output?.greeting).toBe("Hello, world!"); + // The body yields `WorkerEnvironment` — if the regression from PR #71 ever + // returns, the body dies on the first yield and `output` is undefined. + expect(lastStatus.output?.envBindingCount).toBeGreaterThan(0); + }).pipe(logLevel), + { timeout: 30_000 }, +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/assets/index.html b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/assets/index.html new file mode 100644 index 00000000000..8f35316fe87 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/assets/index.html @@ -0,0 +1,10 @@ + + + + Test Worker with Assets + + +

Hello from Worker Assets!

+

This is a test asset file.

+ + diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/assets/test.txt b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/assets/test.txt new file mode 100644 index 00000000000..79edbf80d59 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/assets/test.txt @@ -0,0 +1,3 @@ +This is a test file from assets. +Version: 1 + diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/cron-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/cron-worker.ts new file mode 100644 index 00000000000..ddc7c38f488 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/cron-worker.ts @@ -0,0 +1,73 @@ +import * as Cloudflare from "@/Cloudflare/index.ts"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +/** + * Durable Object that records each `scheduledTime` the cron handler sees. + * The test polls `snapshot()` via the worker's `GET /times` route to verify + * the cron actually fired. + */ +export class CronCounter extends Cloudflare.DurableObjectNamespace()( + "CronCounter", + Effect.gen(function* () { + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + let times = (yield* state.storage.get("times")) ?? []; + return { + record: Effect.fn(function* (time: number) { + times = [...times, time]; + yield* state.storage.put("times", times); + }), + snapshot: () => Effect.succeed({ times }), + reset: Effect.fn(function* () { + times = []; + yield* state.storage.put("times", times); + }), + }; + }); + }), +) {} + +/** + * Fixture worker for `CronEventSource.test.ts`. + * + * Cloudflare's minimum cron granularity is one minute, so the trigger is set + * to `* * * * *`. Each fire records `controller.scheduledTime` on the + * `CronCounter` DO. The test polls `GET /times` until at least one entry + * appears (or the timeout expires). + */ +export default class CronTestWorker extends Cloudflare.Worker()( + "CronTestWorker", + { + main: import.meta.filename, + subdomain: { enabled: true, previewsEnabled: false }, + compatibility: { date: "2024-09-23", flags: ["nodejs_compat"] }, + }, + Effect.gen(function* () { + const counters = yield* CronCounter; + + yield* Cloudflare.cron("* * * * *").subscribe((controller) => + counters.getByName("default").record(controller.scheduledTime), + ); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.url, "http://x"); + + if (request.method === "GET" && url.pathname === "/times") { + const snapshot = yield* counters.getByName("default").snapshot(); + return yield* HttpServerResponse.json(snapshot); + } + + if (request.method === "POST" && url.pathname === "/reset") { + yield* counters.getByName("default").reset(); + return yield* HttpServerResponse.json({ ok: true }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }), + }; + }).pipe(Effect.provide(Cloudflare.CronEventSourceLive)), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/object.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/object.ts new file mode 100644 index 00000000000..7a85b328723 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/object.ts @@ -0,0 +1,30 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import * as Stream from "effect/Stream"; + +const KV = Cloudflare.KVNamespace("DurableObjectWorkerEnvironmentKV", { + title: "durable-object-worker-environment-kv", +}); + +export class WorkerEnvironmentKVObject extends Cloudflare.DurableObjectNamespace()( + "WorkerEnvironmentKVObject", + Effect.gen(function* () { + const kv = yield* Cloudflare.KVNamespace.bind(KV); + + return Effect.gen(function* () { + return { + put: (key: string, value: string) => kv.put(key, value), + get: (key: string) => kv.get(key), + // Mirrors the `tick` example from the tutorial: + // https://v2.alchemy.run/tutorial/cloudflare/durable-objects/ + // An RPC method that returns a Stream of sequential numbers. + tick: (n: number) => + Stream.iterate(0, (i) => i + 1).pipe( + Stream.take(n), + Stream.schedule(Schedule.spaced("100 millis")), + ), + }; + }); + }).pipe(Effect.provide(Cloudflare.KVNamespaceBindingLive)), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/stack.ts new file mode 100644 index 00000000000..39a98c6ce0a --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/stack.ts @@ -0,0 +1,18 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import DurableObjectWorkerEnvironmentWorker from "./worker.ts"; + +export default Alchemy.Stack( + "DurableObjectWorkerEnvironmentStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* DurableObjectWorkerEnvironmentWorker; + return { + url: worker.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/worker.ts new file mode 100644 index 00000000000..6885a269dff --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/do-rpc/worker.ts @@ -0,0 +1,53 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Stream from "effect/Stream"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { WorkerEnvironmentKVObject } from "./object.ts"; + +export default class DurableObjectWorkerEnvironmentWorker extends Cloudflare.Worker()( + "DurableObjectWorkerEnvironmentWorker", + { + main: import.meta.filename, + subdomain: { enabled: true, previewsEnabled: false }, + compatibility: { date: "2024-09-23", flags: ["nodejs_compat"] }, + }, + Effect.gen(function* () { + const objects = yield* WorkerEnvironmentKVObject; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.url, "http://x"); + + if (request.method === "POST" && url.pathname === "/roundtrip") { + const object = objects.getByName("default"); + const key = "durable-object-worker-environment"; + yield* object.put(key, "ok").pipe(Effect.orDie); + const value = yield* object.get(key).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ value }); + } + + // Mirrors the tutorial's `/tick/:n` route verbatim — forwards the + // Stream returned by the DO's `tick` RPC method straight onto the + // HTTP response. + // https://v2.alchemy.run/tutorial/cloudflare/durable-objects/ + if (request.method === "GET" && url.pathname.startsWith("/tick/")) { + const n = Number(url.pathname.split("/").pop()!); + const stream = objects + .getByName("tick") + .tick(n) + .pipe( + Stream.map((i) => `${i}\n`), + Stream.encodeText, + ); + return HttpServerResponse.stream(stream, { + headers: { "content-type": "text/plain" }, + }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/async-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/async-worker.ts new file mode 100644 index 00000000000..7aa9f1125d5 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/async-worker.ts @@ -0,0 +1,25 @@ +import type { AsyncWorkerEnv } from "./stack.ts"; + +/** + * Async (non-Effect) Worker fixture for the Worker Loader binding declared via + * `env: { LOADER: Cloudflare.DynamicWorkerLoader() }`. `InferEnv` maps the + * marker to the native `worker_loader` binding, so the handler calls + * `env.LOADER.load(...).getEntrypoint().fetch(...)` directly. + */ +export default { + async fetch(request: Request, env: AsyncWorkerEnv): Promise { + const worker = env.LOADER.load({ + compatibilityDate: "2026-01-28", + mainModule: "worker.js", + modules: { + "worker.js": `export default { + async fetch() { + return Response.json({ mode: "async", ok: true }); + } + }`, + }, + }); + + return worker.getEntrypoint().fetch(request); + }, +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/effect-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/effect-worker.ts new file mode 100644 index 00000000000..e8fb83d5c42 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/effect-worker.ts @@ -0,0 +1,41 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; + +/** + * Effect-native Worker fixture for the Worker Loader binding. Yielding + * `Cloudflare.DynamicWorkerLoader(name)` during Init registers the + * `worker_loader` binding on this Worker and returns the runtime handle in one + * step — no separate `.bind(...)`. The fetch handler loads an isolated dynamic + * Worker from inline source and proxies the request to it over Effect-native + * HTTP. + */ +export default class DynamicLoaderEffectWorker extends Cloudflare.Worker()( + "DynamicLoaderEffectWorker", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const loader = yield* Cloudflare.DynamicWorkerLoader("LOADER"); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + + const worker = loader.load({ + compatibilityDate: "2026-01-28", + mainModule: "worker.js", + modules: { + "worker.js": `export default { + async fetch() { + return Response.json({ mode: "effect", ok: true }); + } + }`, + }, + }); + + return yield* worker.fetch(request).pipe(Effect.orDie); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/stack.ts new file mode 100644 index 00000000000..c462387e84b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/dynamic-worker-loader/stack.ts @@ -0,0 +1,31 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as pathe from "pathe"; +import DynamicLoaderEffectWorker from "./effect-worker.ts"; + +export const AsyncWorker = Cloudflare.Worker("DynamicLoaderAsyncWorker", { + main: pathe.resolve(import.meta.dirname, "async-worker.ts"), + env: { + LOADER: Cloudflare.DynamicWorkerLoader(), + }, +}); + +export type AsyncWorkerEnv = Cloudflare.InferEnv; + +export default Alchemy.Stack( + "DynamicWorkerLoaderStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const asyncWorker = yield* AsyncWorker; + const effectWorker = yield* DynamicLoaderEffectWorker; + + return { + asyncWorkerUrl: asyncWorker.url.as(), + effectWorkerUrl: effectWorker.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/async.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/async.ts new file mode 100644 index 00000000000..16a6ccbc8cc --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/async.ts @@ -0,0 +1,32 @@ +import type { AsyncWorkerEnv } from "./stack.ts"; + +// Async (non-Effect) Worker handler that echoes its env bindings back as +// JSON so the test can assert that every supported `WorkerBindingResource` +// shape (string, number, boolean, null, array, object, Redacted, +// Redacted, Config, Config) round-trips end-to-end. +export default { + fetch: async (_request: Request, env: AsyncWorkerEnv) => { + return new Response( + JSON.stringify({ + STR: env.STR, + NUM: env.NUM, + BOOL: env.BOOL, + NULL: env.NULL, + OBJ: env.OBJ, + ARR: env.ARR, + SECRET_STR: env.SECRET_STR, + // Redacted is JSON-stringified into secret_text on the way in, + // so the async runtime sees a string here. Parse it back so the + // test can compare the structured value. + SECRET_JSON: + typeof env.SECRET_JSON === "string" + ? JSON.parse(env.SECRET_JSON) + : env.SECRET_JSON, + CONFIG_STR: env.CONFIG_STR, + CONFIG_NUM: env.CONFIG_NUM, + CONFIG_REDACTED: env.CONFIG_REDACTED, + }), + { headers: { "content-type": "application/json" } }, + ); + }, +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/effect.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/effect.ts new file mode 100644 index 00000000000..3a546e5066e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/effect.ts @@ -0,0 +1,82 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Config from "effect/Config"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +/** + * Effect-native Worker fixture that exercises every supported + * `WorkerBindingResource` shape and echoes the resolved values back as + * JSON so the test can assert the deploy → runtime round-trip. + * + * Mixes the two declaration styles: + * - `env: { ... }` literal/Redacted/Config values declared on the + * resource (resolved via `WorkerEnvironment` at runtime). + * - `yield* Config.xxx(...)` resolved during Init — Alchemy captures + * the binding automatically and the same `Config` re-resolves from + * the binding at runtime. + */ +export default class EnvEffectWorker extends Cloudflare.Worker()( + "EnvEffectWorker", + { + main: import.meta.filename, + env: { + STR: "hello", + NUM: 42, + BOOL: true, + NULL: null, + OBJ: { nested: { value: "ok" }, count: 7 }, + ARR: [1, 2, 3], + SECRET_STR: Redacted.make("shh"), + SECRET_JSON: Redacted.make({ token: "abc", scopes: ["read", "write"] }), + // Config declared statically on `env` — Alchemy resolves at deploy + // time and binds it as `secret_text` on the Worker. + CONFIG_REDACTED: Config.redacted("CONFIG_REDACTED"), + }, + }, + Effect.gen(function* () { + // Captured during Init — Alchemy binds these onto the Worker and the + // runtime ConfigProvider (backed by `env`) re-resolves them here. + const configStr = yield* Config.string("CONFIG_STR"); + const configNum = yield* Config.number("CONFIG_NUM"); + const configRedactedInit = yield* Config.redacted("CONFIG_REDACTED_INIT"); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const env = yield* Cloudflare.WorkerEnvironment; + const pathname = new URL(request.originalUrl).pathname; + + if (pathname === "/env") { + return yield* HttpServerResponse.json({ + STR: env.STR, + NUM: env.NUM, + BOOL: env.BOOL, + NULL: env.NULL, + OBJ: env.OBJ, + ARR: env.ARR, + SECRET_STR: env.SECRET_STR, + SECRET_JSON: + typeof env.SECRET_JSON === "string" + ? JSON.parse(env.SECRET_JSON) + : env.SECRET_JSON, + }); + } + + if (pathname === "/config") { + return yield* HttpServerResponse.json({ + CONFIG_STR: configStr, + CONFIG_NUM: configNum, + CONFIG_REDACTED: env.CONFIG_REDACTED, + CONFIG_REDACTED_INIT: Redacted.value(configRedactedInit), + CONFIG_REDACTED_INIT_IS_REDACTED: + Redacted.isRedacted(configRedactedInit), + }); + } + + return HttpServerResponse.text("ok"); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/stack.ts new file mode 100644 index 00000000000..ce9978a8837 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/env/stack.ts @@ -0,0 +1,47 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Config from "effect/Config"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as path from "pathe"; +import EnvEffectWorker from "./effect.ts"; + +export type AsyncWorkerEnv = Cloudflare.InferEnv; + +export const AsyncWorker = Cloudflare.Worker("EnvAsyncWorker", { + main: path.resolve(import.meta.dirname, "async.ts"), + url: true, + env: { + STR: "hello", + NUM: 42, + BOOL: true, + NULL: null, + OBJ: { nested: { value: "ok" }, count: 7 }, + ARR: [1, 2, 3], + SECRET_STR: Redacted.make("shh"), + SECRET_JSON: Redacted.make({ + token: "abc", + scopes: ["read", "write"], + }), + CONFIG_STR: Config.string("CONFIG_STR"), + CONFIG_NUM: Config.number("CONFIG_NUM"), + CONFIG_REDACTED: Config.redacted("CONFIG_REDACTED"), + }, +}); + +export default Alchemy.Stack( + "WorkerEnvTestStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const asyncWorker = yield* AsyncWorker; + const effectWorker = yield* EnvEffectWorker; + + return { + asyncUrl: asyncWorker.url.as(), + effectUrl: effectWorker.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/api.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/api.ts new file mode 100644 index 00000000000..6424328b919 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/api.ts @@ -0,0 +1,58 @@ +import * as Schema from "effect/Schema"; +import * as HttpApi from "effect/unstable/httpapi/HttpApi"; +import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint"; +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup"; + +export class Task extends Schema.Class("Task")({ + id: Schema.String, + title: Schema.String, + completed: Schema.Boolean, +}) {} + +export const decodeTask = Schema.decodeUnknownEffect(Task); + +export const encodeTask = Schema.encodeUnknownSync(Task); + +export class TaskNotFound extends Schema.TaggedErrorClass()( + "TaskNotFound", + { id: Schema.String }, + { httpApiStatus: 404 }, +) {} + +const TaskParams = Schema.Struct({ + id: Schema.String, +}); + +export const getTask = HttpApiEndpoint.get("getTask", "/:id", { + params: TaskParams, + success: Task, + error: TaskNotFound, +}); + +export const createTask = HttpApiEndpoint.post("createTask", "/", { + success: Task, + payload: Schema.Struct({ + title: Schema.String, + }), +}); + +const getTaskDO = HttpApiEndpoint.get("getTaskDO", "/do/:id", { + params: TaskParams, + success: Task, + error: TaskNotFound, +}); + +const createTaskDO = HttpApiEndpoint.post("createTaskDO", "/do", { + success: Task, + payload: Schema.Struct({ + title: Schema.String, + }), +}); + +export class TasksGroup extends HttpApiGroup.make("Tasks") + .add(getTask) + .add(createTask) + .add(getTaskDO) + .add(createTaskDO) {} + +export class TaskApi extends HttpApi.make("TaskApi").add(TasksGroup) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/object.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/object.ts new file mode 100644 index 00000000000..402ebf7cf27 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/object.ts @@ -0,0 +1,59 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import { Layer } from "effect"; +import * as Effect from "effect/Effect"; +import { HttpRouter } from "effect/unstable/http"; +import * as HttpApi from "effect/unstable/httpapi/HttpApi"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; +import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup"; + +import { createTask, decodeTask, encodeTask, getTask, Task } from "./api.ts"; + +export class TasksDOGroup extends HttpApiGroup.make("TasksDO") + .add(getTask) + .add(createTask) {} + +export class TaskDOApi extends HttpApi.make("TaskDOApi").add(TasksDOGroup) {} + +/** + * Durable Object backing the `createTaskDO` / `getTaskDO` endpoints. + * Persists tasks in the DO's transactional storage and exposes simple + * RPC methods that the Worker calls from its HttpApi handlers. + */ +export default class TasksObject extends Cloudflare.DurableObjectNamespace()( + "TasksObject", + Effect.gen(function* () { + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + const tasksGroup = HttpApiBuilder.group( + TaskDOApi, + "TasksDO", + (handlers) => + handlers + .handle("getTask", ({ params }) => + state.storage + .get(params.id) + .pipe(Effect.flatMap(decodeTask), Effect.orDie), + ) + .handle("createTask", ({ payload }) => { + const id = crypto.randomUUID(); + const task = new Task({ + id, + title: payload.title, + completed: false, + }); + return state.storage + .put(id, encodeTask(task)) + .pipe(Effect.as(task)); + }), + ); + + return { + fetch: HttpApiBuilder.layer(TaskDOApi).pipe( + Layer.provide(tasksGroup), + HttpRouter.toHttpEffect, + ), + }; + }); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/stack.ts new file mode 100644 index 00000000000..6011385a02b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/stack.ts @@ -0,0 +1,18 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import HttpApiTestWorker from "./worker.ts"; + +export default Alchemy.Stack( + "HttpApiTestStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* HttpApiTestWorker; + return { + url: worker.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/worker.ts new file mode 100644 index 00000000000..7040384078f --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/http-api/worker.ts @@ -0,0 +1,100 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Etag from "effect/unstable/http/Etag"; +import * as HttpPlatform from "effect/unstable/http/HttpPlatform"; +import * as HttpRouter from "effect/unstable/http/HttpRouter"; +import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; +import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; +import { decodeTask, Task, TaskApi, TaskNotFound } from "./api.ts"; +import TasksObject, { TaskDOApi } from "./object.ts"; + +const HttpPlatformStub = Layer.succeed(HttpPlatform.HttpPlatform, { + fileResponse: () => Effect.die("HttpPlatform.fileResponse not supported"), + fileWebResponse: () => + Effect.die("HttpPlatform.fileWebResponse not supported"), +}); + +const corsLayer = HttpRouter.cors({ + allowedOrigins: ["*"], + allowedMethods: ["GET", "POST", "OPTIONS"], + allowedHeaders: ["Content-Type"], +}); + +const Bucket = Cloudflare.R2Bucket("Tasks"); + +export default class HttpApiTestWorker extends Cloudflare.Worker()( + "HttpApiTestWorker", + { + main: import.meta.filename, + subdomain: { enabled: true, previewsEnabled: false }, + compatibility: { date: "2024-09-23", flags: ["nodejs_compat"] }, + }, + Effect.gen(function* () { + const tasks = yield* Cloudflare.R2Bucket.bind(Bucket); + const tasksDO = yield* TasksObject; + + const getTaskDO = (id: string = "default") => + HttpApiClient.makeWith(TaskDOApi, { + baseUrl: `http://localhost`, + httpClient: Cloudflare.toHttpClient(tasksDO.getByName(id)), + }); + + const tasksGroup = HttpApiBuilder.group(TaskApi, "Tasks", (handlers) => + handlers + .handle("getTask", ({ params }) => + tasks.get(params.id).pipe( + Effect.orDie, + Effect.flatMap((data) => + data + ? data.text().pipe( + Effect.map((data) => JSON.parse(data)), + Effect.orDie, + ) + : Effect.succeed(undefined), + ), + Effect.flatMap((data) => + data + ? decodeTask(data).pipe(Effect.orDie) + : Effect.fail(new TaskNotFound({ id: params.id })), + ), + Effect.tapError((err) => Effect.logError("err", err)), + ), + ) + .handle("createTask", ({ payload }) => { + const task = new Task({ + id: crypto.randomUUID(), + title: payload.title, + completed: false, + }); + return tasks + .put(task.id, JSON.stringify(task)) + .pipe(Effect.orDie, Effect.as(task)); + }) + .handle("getTaskDO", ({ params }) => + getTaskDO().pipe( + Effect.flatMap((client) => + client.TasksDO.getTask({ params }).pipe(Effect.orDie), + ), + ), + ) + .handle("createTaskDO", ({ payload }) => + getTaskDO().pipe( + Effect.flatMap((client) => + client.TasksDO.createTask({ payload }).pipe(Effect.orDie), + ), + ), + ), + ); + + return { + fetch: HttpApiBuilder.layer(TaskApi).pipe( + Layer.provide(tasksGroup), + Layer.provide([Etag.layer, HttpPlatformStub, Path.layer]), + Layer.provide(corsLayer), + HttpRouter.toHttpEffect, + ), + }; + }).pipe(Effect.provide(Cloudflare.R2BucketBindingLive)), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/internal-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/internal-worker.ts new file mode 100644 index 00000000000..3b73658f0ca --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/internal-worker.ts @@ -0,0 +1,19 @@ +import * as Cloudflare from "@/Cloudflare/index.ts"; +import * as Effect from "effect/Effect"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +export default class InternalWorker extends Cloudflare.Worker()( + "InternalWorker", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + return { + fetch: Effect.gen(function* () { + return HttpServerResponse.text("Hello from InternalWorker", { + status: 200, + }); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/group.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/group.ts new file mode 100644 index 00000000000..c3c70efd4cb --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/group.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import * as Rpc from "effect/unstable/rpc/Rpc"; +import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; +import * as RpcSchema from "effect/unstable/rpc/RpcSchema"; + +/** + * Procedures served on each {@link RpcDurableObjectNamespace} instance. + * The DO instance *is* the session, so payloads don't include a per- + * session id — the keying happens at `getByName(...)`. + */ +export class CounterRpcs extends RpcGroup.make( + Rpc.make("Increment", { + payload: {}, + success: Schema.Struct({ count: Schema.Number }), + }), + Rpc.make("Get", { + payload: {}, + success: Schema.Struct({ count: Schema.Number }), + }), + Rpc.make("CountUpTo", { + payload: { upto: Schema.Number }, + success: RpcSchema.Stream(Schema.Number, Schema.Never), + }), + Rpc.make("Reset", { + payload: {}, + success: Schema.Void, + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/object.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/object.ts new file mode 100644 index 00000000000..b5851936c14 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/object.ts @@ -0,0 +1,47 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { CounterRpcs } from "./group.ts"; + +/** + * Typed counter Durable Object built on + * {@link Cloudflare.RpcDurableObjectNamespace}. Persists `count` in + * `state.storage` and serves `Increment` / `Get` / `CountUpTo` over + * an `RpcServer.toHttpEffect(group)` on the DO's `fetch`. + */ +export default class RpcCounterObject extends Cloudflare.RpcDurableObjectNamespace()( + "RpcCounterObject", + { schema: CounterRpcs }, + Effect.gen(function* () { + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + let count = (yield* state.storage.get("count")) ?? 0; + + const handlers = CounterRpcs.toLayer({ + Increment: () => + Effect.gen(function* () { + count += 1; + yield* state.storage.put("count", count); + return { count }; + }), + Get: () => Effect.succeed({ count }), + CountUpTo: ({ upto }) => + Stream.fromIterable( + Array.from({ length: Math.max(0, upto) }, (_, i) => i + 1), + ), + Reset: () => + Effect.gen(function* () { + count = 0; + yield* state.storage.put("count", 0); + }), + }); + + return RpcServer.toHttpEffect(CounterRpcs).pipe( + Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), + ); + }); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/stack.ts new file mode 100644 index 00000000000..b477186647a --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/stack.ts @@ -0,0 +1,21 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import RpcCounterWorker from "./worker.ts"; + +/** + * Stack with one Worker driving an + * {@link Cloudflare.RpcDurableObjectNamespace} counter via the typed + * `getByName(id)` client. + */ +export default Alchemy.Stack( + "RpcDurableObjectNamespaceStack", + { + providers: Cloudflare.providers(), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const worker = yield* RpcCounterWorker; + return { url: worker.url.as() }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/worker.ts new file mode 100644 index 00000000000..b26e29a825a --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-do-namespace-do-rpc/worker.ts @@ -0,0 +1,70 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Stream from "effect/Stream"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import RpcCounterObject from "./object.ts"; + +/** + * Plain {@link Cloudflare.Worker} that drives the + * {@link RpcCounterObject} via the typed `getByName(id)` client returned + * from {@link Cloudflare.RpcDurableObjectNamespace}. Each route maps to + * one RPC, so the integ test can assert the round-trip end to end: + * + * - `POST /counter/:id/increment` → `Increment` + * - `GET /counter/:id` → `Get` + * - `GET /counter/:id/stream?upto=N` → `CountUpTo` (newline-delimited) + */ +export default class RpcCounterWorker extends Cloudflare.Worker()( + "RpcCounterWorker", + { + main: import.meta.filename, + subdomain: { enabled: true, previewsEnabled: false }, + compatibility: { date: "2024-09-23", flags: ["nodejs_compat"] }, + }, + Effect.gen(function* () { + const counters = yield* RpcCounterObject; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.url, "http://x"); + const match = url.pathname.match(/^\/counter\/([^/]+)(?:\/(\w+))?$/); + if (!match) + return HttpServerResponse.text("Not Found", { status: 404 }); + const [, id, action] = match; + + if (request.method === "POST" && action === "increment") { + const client = yield* counters.getByName(id); + const result = yield* client.Increment({}).pipe(Effect.orDie); + return yield* HttpServerResponse.json(result); + } + if (request.method === "POST" && action === "reset") { + const client = yield* counters.getByName(id); + yield* client.Reset({}).pipe(Effect.orDie); + return HttpServerResponse.text("ok"); + } + if (request.method === "GET" && action === "stream") { + const upto = Number(url.searchParams.get("upto") ?? "5"); + const body = Stream.unwrap( + Effect.map(counters.getByName(id), (client) => + client.CountUpTo({ upto }).pipe( + Stream.map((n) => new TextEncoder().encode(`${n}\n`)), + Stream.orDie, + ), + ), + ); + return HttpServerResponse.stream(body, { + headers: { "content-type": "text/plain" }, + }); + } + if (request.method === "GET" && action === undefined) { + const client = yield* counters.getByName(id); + const result = yield* client.Get({}).pipe(Effect.orDie); + return yield* HttpServerResponse.json(result); + } + return HttpServerResponse.text("Not Found", { status: 404 }); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/group.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/group.ts new file mode 100644 index 00000000000..7d6e4d2be4f --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/group.ts @@ -0,0 +1,58 @@ +import * as Schema from "effect/Schema"; +import * as Rpc from "effect/unstable/rpc/Rpc"; +import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; +import * as RpcSchema from "effect/unstable/rpc/RpcSchema"; + +/** + * RPCs implemented locally in both the Worker and the Durable Object. + * + * - `Ping` — unary + * - `Count` — streaming response + * - `Echo` — array payload (a "stream" of inputs in one HTTP body) that + * replays each item back as a streaming response + */ +export const InnerRpcs = RpcGroup.make( + Rpc.make("Ping", { + payload: { message: Schema.String }, + success: Schema.Struct({ echo: Schema.String, n: Schema.Number }), + }), + Rpc.make("Count", { + payload: { upto: Schema.Number }, + success: RpcSchema.Stream(Schema.Number, Schema.Never), + }), + Rpc.make("Echo", { + payload: { messages: Schema.Array(Schema.String) }, + success: RpcSchema.Stream( + Schema.Struct({ index: Schema.Number, message: Schema.String }), + Schema.Never, + ), + }), +); + +/** + * RPCs exposed only by the Worker. Each handler proxies the corresponding + * `InnerRpcs` method through an `RpcClient` whose transport is backed by + * `Cloudflare.toHttpClient(rpcDO.getByName(...))`. This exercises the DO + * fetch pathway (`DurableObjectBridge.fetch` -> `makeRequestEffect`) via + * the typed RPC client, mirroring the `*DO` variants in the HttpApi + * fixture. + */ +export const DoRpcs = RpcGroup.make( + Rpc.make("PingDO", { + payload: { message: Schema.String }, + success: Schema.Struct({ echo: Schema.String, n: Schema.Number }), + }), + Rpc.make("CountDO", { + payload: { upto: Schema.Number }, + success: RpcSchema.Stream(Schema.Number, Schema.Never), + }), + Rpc.make("EchoDO", { + payload: { messages: Schema.Array(Schema.String) }, + success: RpcSchema.Stream( + Schema.Struct({ index: Schema.Number, message: Schema.String }), + Schema.Never, + ), + }), +); + +export const WorkerRpcs = InnerRpcs.merge(DoRpcs); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/object.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/object.ts new file mode 100644 index 00000000000..65b0c400e7e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/object.ts @@ -0,0 +1,63 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { DoRpcs } from "./group.ts"; + +/** + * Durable Object backing the `*DO` RPCs on the Worker. It exposes the + * `InnerRpcs` group via `RpcServer.toHttpEffect`. The Worker constructs + * an `RpcClient` whose transport calls into this DO's `fetch`, so the + * Worker's `*DO` handlers transparently delegate here. + */ +export default class RpcHttpTestObject extends Cloudflare.DurableObjectNamespace()( + "RpcHttpTestObject", + Effect.gen(function* () { + return Effect.gen(function* () { + let counter = 0; + + const handlersLayer = DoRpcs.toLayer({ + PingDO: ({ message }) => { + console.log("Ping"); + return Effect.sync(() => { + console.log("Ping inside"); + return { + echo: message, + n: ++counter, + }; + }); + }, + CountDO: ({ upto }) => + Stream.fromReadableStream({ + evaluate: () => { + let next = 1; + return new ReadableStream({ + pull(controller) { + if (next > upto) { + controller.close(); + return; + } + controller.enqueue(next++); + }, + }); + }, + onError: (cause) => cause as never, + }), + EchoDO: ({ messages }) => + Stream.fromIterable( + messages.map((message, index) => ({ index, message })), + ), + }); + + return { + fetch: RpcServer.toHttpEffect(DoRpcs).pipe( + Effect.provide( + Layer.mergeAll(handlersLayer, RpcSerialization.layerNdjson), + ), + ), + }; + }); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/stack.ts new file mode 100644 index 00000000000..6554a2db0f0 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/stack.ts @@ -0,0 +1,18 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import RpcHttpTestWorker from "./worker.ts"; + +export default Alchemy.Stack( + "RpcHttpTestStack", + { + providers: Cloudflare.providers(), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const worker = yield* RpcHttpTestWorker; + return { + url: worker.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/worker.ts new file mode 100644 index 00000000000..c646a4ee82b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-http/worker.ts @@ -0,0 +1,97 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import type { HttpEffect } from "alchemy/Http"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as RpcClient from "effect/unstable/rpc/RpcClient"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { DoRpcs, WorkerRpcs } from "./group.ts"; +import RpcHttpTestObject from "./object.ts"; + +let counter = 0; + +export default class RpcHttpTestWorker extends Cloudflare.Worker()( + "RpcHttpTestWorker", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const rpcDO = yield* RpcHttpTestObject; + + // Per-request RpcClient transport: dispatches HTTP requests to the + // DO's fetch handler (which serves `InnerRpcs` via + // `RpcServer.toHttpEffect`). Mirrors the `Cloudflare.toHttpClient` + // pattern used by the HttpApi fixture's `getTaskDO` factory. + const makeDOClient = (id: string = "default") => + RpcClient.make(DoRpcs).pipe( + Effect.provide( + RpcClient.layerProtocolHttp({ url: "http://localhost" }).pipe( + Layer.provide( + Layer.succeed( + HttpClient.HttpClient, + Cloudflare.toHttpClient(rpcDO.getByName(id)), + ), + ), + Layer.provide(RpcSerialization.layerNdjson), + ), + ), + ); + + const handlersLayer = WorkerRpcs.toLayer({ + Ping: ({ message }) => + Effect.sync(() => ({ + echo: message, + n: ++counter, + })), + Count: ({ upto }) => + Stream.fromReadableStream({ + evaluate: () => { + let next = 1; + return new ReadableStream({ + pull(controller) { + if (next > upto) { + controller.close(); + return; + } + controller.enqueue(next++); + }, + }); + }, + onError: (cause) => cause as never, + }), + Echo: ({ messages }) => + Stream.fromIterable( + messages.map((message, index) => ({ index, message })), + ), + PingDO: (payload) => + Effect.gen(function* () { + const client = yield* makeDOClient(); + const result = yield* client.PingDO(payload); + return result; + }).pipe(Effect.orDie), + CountDO: (payload) => + Stream.unwrap( + Effect.gen(function* () { + const client = yield* makeDOClient(); + return client.CountDO(payload).pipe(Stream.orDie); + }), + ), + EchoDO: (payload) => + Stream.unwrap( + Effect.gen(function* () { + return (yield* makeDOClient()).EchoDO(payload).pipe(Stream.orDie); + }), + ), + }); + + return { + fetch: RpcServer.toHttpEffect(WorkerRpcs).pipe( + Effect.provide( + Layer.mergeAll(handlersLayer, RpcSerialization.layerNdjson), + ), + ) as unknown as HttpEffect, + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/caller-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/caller-worker.ts new file mode 100644 index 00000000000..a1521f77efe --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/caller-worker.ts @@ -0,0 +1,31 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { CallerRpcs } from "./group.ts"; +import BindingTargetRpcWorker from "./target-worker.ts"; + +/** + * Caller {@link RpcWorker} that uses `Cloudflare.RpcWorker.bind` to + * obtain a typed client for {@link BindingTargetRpcWorker}. The bind + * yields the client directly — the per-request handler just calls + * `target.Greet({ name })` like any other Effect RpcClient. Internally + * each method invocation builds a fresh underlying RPC client (via a + * Proxy) because Cloudflare rejects cross-request reuse of the + * service-binding stub I/O; that detail is hidden from the consumer. + */ +export default class BindingCallerRpcWorker extends Cloudflare.RpcWorker()( + "BindingCallerRpcWorker", + { main: import.meta.filename, schema: CallerRpcs }, + Effect.gen(function* () { + const target = yield* Cloudflare.RpcWorker.bind(BindingTargetRpcWorker); + + const handlers = CallerRpcs.toLayer({ + ProxyGreet: ({ name }) => target.Greet({ name }).pipe(Effect.orDie), + }); + return RpcServer.toHttpEffect(CallerRpcs).pipe( + Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), + ); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/group.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/group.ts new file mode 100644 index 00000000000..7df85096be5 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/group.ts @@ -0,0 +1,25 @@ +import * as Schema from "effect/Schema"; +import * as Rpc from "effect/unstable/rpc/Rpc"; +import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; + +/** + * RPCs exposed by the target {@link RpcWorker} — the canonical + * `greet(name)` so the call site has an obvious round-trip to assert. + */ +export class TargetRpcs extends RpcGroup.make( + Rpc.make("Greet", { + payload: { name: Schema.String }, + success: Schema.Struct({ greeting: Schema.String }), + }), +) {} + +/** + * RPCs exposed by the caller worker. `ProxyGreet` forwards through the + * service-binding RPC client returned by `Cloudflare.RpcWorker.bind`. + */ +export class CallerRpcs extends RpcGroup.make( + Rpc.make("ProxyGreet", { + payload: { name: Schema.String }, + success: Schema.Struct({ greeting: Schema.String }), + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/stack.ts new file mode 100644 index 00000000000..77514d714f3 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/stack.ts @@ -0,0 +1,30 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import BindingCallerRpcWorker from "./caller-worker.ts"; +import BindingTargetRpcWorker from "./target-worker.ts"; + +/** + * Stack with two `RpcWorker`s wired through `RpcWorker.bind`: + * + * - `BindingTargetRpcWorker` — exposes `Greet`. + * - `BindingCallerRpcWorker` — exposes `ProxyGreet`, which yields + * `Cloudflare.RpcWorker.bind(BindingTargetRpcWorker)` and forwards + * the call through the typed RPC client over the in-account service + * binding. + */ +export default Alchemy.Stack( + "RpcWorkerBindingStack", + { + providers: Cloudflare.providers(), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const target = yield* BindingTargetRpcWorker; + const caller = yield* BindingCallerRpcWorker; + return { + targetUrl: target.url.as(), + callerUrl: caller.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/target-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/target-worker.ts new file mode 100644 index 00000000000..eff61afc205 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-binding/target-worker.ts @@ -0,0 +1,25 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { TargetRpcs } from "./group.ts"; + +/** + * Effect-native `RpcWorker` that exposes `greet(name)`. Used as the + * *callee* in the RpcWorker-binding fixture — the caller worker + * yields `Cloudflare.RpcWorker.bind(BindingTargetRpcWorker)` to obtain + * a typed `RpcClient`. + */ +export default class BindingTargetRpcWorker extends Cloudflare.RpcWorker()( + "BindingTargetRpcWorker", + { main: import.meta.filename, schema: TargetRpcs }, + Effect.gen(function* () { + const handlers = TargetRpcs.toLayer({ + Greet: ({ name }) => Effect.succeed({ greeting: `hello ${name}` }), + }); + return RpcServer.toHttpEffect(TargetRpcs).pipe( + Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), + ); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/group.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/group.ts new file mode 100644 index 00000000000..b50ef90d205 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/group.ts @@ -0,0 +1,42 @@ +import * as Schema from "effect/Schema"; +import * as Rpc from "effect/unstable/rpc/Rpc"; +import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; +import * as RpcSchema from "effect/unstable/rpc/RpcSchema"; + +/** + * RPCs implemented locally on the {@link RpcWorker} itself. + * + * - `Ping` — unary + * - `Count` — streaming response + */ +export class InnerRpcs extends RpcGroup.make( + Rpc.make("Ping", { + payload: { message: Schema.String }, + success: Schema.Struct({ echo: Schema.String, n: Schema.Number }), + }), + Rpc.make("Count", { + payload: { upto: Schema.Number }, + success: RpcSchema.Stream(Schema.Number, Schema.Never), + }), +) {} + +/** + * RPCs served by the {@link RpcDurableObjectNamespace} backing the + * `*DO` variants below — same shapes as `InnerRpcs`, sharing the wire + * codec end-to-end. + */ +export class DoRpcs extends RpcGroup.make( + Rpc.make("PingDO", { + payload: { message: Schema.String }, + success: Schema.Struct({ echo: Schema.String, n: Schema.Number }), + }), + Rpc.make("CountDO", { + payload: { upto: Schema.Number }, + success: RpcSchema.Stream(Schema.Number, Schema.Never), + }), +) {} + +/** + * Surface served by the {@link RpcWorker} — local + DO-proxied. + */ +export const WorkerRpcs = InnerRpcs.merge(DoRpcs); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/object.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/object.ts new file mode 100644 index 00000000000..7a1653d4b37 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/object.ts @@ -0,0 +1,37 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { DoRpcs } from "./group.ts"; + +/** + * Typed Durable Object backing the `*DO` variants on the + * {@link RpcWorkerRpcHttpWorker}. Built with + * {@link Cloudflare.RpcDurableObjectNamespace} so the worker can call + * `objects.getByName(id).PingDO(...)` directly — no `RpcClient.make`, + * no `Cloudflare.toHttpClient`, no manual transport plumbing. + */ +export default class RpcWorkerRpcHttpObject extends Cloudflare.RpcDurableObjectNamespace()( + "RpcWorkerRpcHttpObject", + { schema: DoRpcs }, + Effect.gen(function* () { + return Effect.gen(function* () { + let counter = 0; + + const handlers = DoRpcs.toLayer({ + PingDO: ({ message }) => + Effect.sync(() => ({ echo: message, n: ++counter })), + CountDO: ({ upto }) => + Stream.fromIterable( + Array.from({ length: Math.max(0, upto) }, (_, i) => i + 1), + ), + }); + + return RpcServer.toHttpEffect(DoRpcs).pipe( + Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), + ); + }); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/stack.ts new file mode 100644 index 00000000000..c098fa6be3b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/stack.ts @@ -0,0 +1,20 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import RpcWorkerRpcHttpWorker from "./worker.ts"; + +/** + * Stack for the {@link Cloudflare.RpcWorker} + + * {@link Cloudflare.RpcDurableObjectNamespace} combined fixture. + */ +export default Alchemy.Stack( + "RpcWorkerRpcHttpStack", + { + providers: Cloudflare.providers(), + state: Alchemy.localState(), + }, + Effect.gen(function* () { + const worker = yield* RpcWorkerRpcHttpWorker; + return { url: worker.url.as() }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/worker.ts new file mode 100644 index 00000000000..7e16de90650 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/rpc-worker-rpc-http/worker.ts @@ -0,0 +1,51 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { WorkerRpcs } from "./group.ts"; +import RpcWorkerRpcHttpObject from "./object.ts"; + +let counter = 0; + +/** + * {@link Cloudflare.RpcWorker} variant of the manual `rpc-http` + * fixture. Local handlers (`Ping`, `Count`) live on the worker; `*DO` + * handlers forward through the typed + * {@link RpcWorkerRpcHttpObject} client returned by yielding the + * {@link Cloudflare.RpcDurableObjectNamespace} class. The piped + * `RpcServer.toHttpEffect(WorkerRpcs)` is returned directly — no + * `{ fetch }` wrapper. + */ +export default class RpcWorkerRpcHttpWorker extends Cloudflare.RpcWorker()( + "RpcWorkerRpcHttpWorker", + { main: import.meta.filename, schema: WorkerRpcs }, + Effect.gen(function* () { + const objects = yield* RpcWorkerRpcHttpObject; + + const handlers = WorkerRpcs.toLayer({ + Ping: ({ message }) => + Effect.sync(() => ({ echo: message, n: ++counter })), + Count: ({ upto }) => + Stream.fromIterable( + Array.from({ length: Math.max(0, upto) }, (_, i) => i + 1), + ), + PingDO: ({ message }) => + Effect.gen(function* () { + const client = yield* objects.getByName("default"); + return yield* client.PingDO({ message }); + }).pipe(Effect.orDie), + CountDO: ({ upto }) => + Stream.unwrap( + Effect.map(objects.getByName("default"), (client) => + client.CountDO({ upto }).pipe(Stream.orDie), + ), + ), + }); + + return RpcServer.toHttpEffect(WorkerRpcs).pipe( + Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), + ); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/object.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/object.ts new file mode 100644 index 00000000000..db1c2c527f4 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/object.ts @@ -0,0 +1,84 @@ +import type { RuntimeContext } from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export const MyDB = Cloudflare.D1Database("MyDB"); + +const DO_COUNT_KEY = "do_count"; + +// Tag — D1 RPC methods take an explicit row `key` so the shared D1 table +// can be partitioned per-caller. The DO storage methods don't need one +// because per-instance storage is already isolated by `getByName(name)`. +export class Counter extends Cloudflare.DurableObjectNamespace< + Counter, + { + incrementD1: (key: string) => Effect.Effect; + getD1: (key: string) => Effect.Effect; + incrementDO: () => Effect.Effect; + getDO: () => Effect.Effect; + reset: (key: string) => Effect.Effect; + } +>()("Counter") {} + +// Layer +export const CounterLive = Counter.make( + Effect.gen(function* () { + const db = yield* Cloudflare.D1Connection.bind(MyDB); + + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + // D1's `exec()` splits on newlines and rejects multi-line statements + // ("incomplete input: SQLITE_ERROR"). Keep the DDL on a single line. + yield* db.exec( + "CREATE TABLE IF NOT EXISTS d1_counters (id TEXT PRIMARY KEY, value INTEGER NOT NULL DEFAULT 0)", + ); + + const readD1 = (key: string) => + db + .prepare("SELECT value FROM d1_counters WHERE id = ?") + .bind(key) + .first<{ value: number }>() + .pipe(Effect.map((row) => row?.value ?? 0)); + + const writeD1 = (key: string, value: number) => + db + .prepare( + "INSERT INTO d1_counters (id, value) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value", + ) + .bind(key, value) + .run() + .pipe(Effect.asVoid); + + const readDO = () => + state.storage + .get(DO_COUNT_KEY) + .pipe(Effect.map((value) => value ?? 0)); + + const writeDO = (value: number) => + state.storage.put(DO_COUNT_KEY, value).pipe(Effect.asVoid); + + return { + incrementD1: (key: string) => + Effect.gen(function* () { + const next = (yield* readD1(key)) + 1; + yield* writeD1(key, next); + return next; + }), + getD1: (key: string) => readD1(key), + incrementDO: () => + Effect.gen(function* () { + const next = (yield* readDO()) + 1; + yield* writeDO(next); + return next; + }), + getDO: () => readDO(), + reset: (key: string) => + Effect.gen(function* () { + yield* writeD1(key, 0); + yield* writeDO(0); + }), + }; + }); + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/stack.ts new file mode 100644 index 00000000000..004936cbaa8 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/stack.ts @@ -0,0 +1,25 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import WorkerALayer, { WorkerA } from "./workerA.ts"; +import WorkerB from "./workerB.ts"; +import WorkerCLayer, { WorkerC } from "./workerC.ts"; + +export default Alchemy.Stack( + "TaggedDOExample", + { + state: Cloudflare.state(), + providers: Cloudflare.providers(), + }, + Effect.gen(function* () { + const workerA = yield* WorkerA; + const workerB = yield* WorkerB; + const workerC = yield* WorkerC; + + return { + urlA: workerA.url.as(), + urlB: workerB.url.as(), + urlC: workerC.url.as(), + }; + }).pipe(Effect.provide(WorkerALayer), Effect.provide(WorkerCLayer)), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerA.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerA.ts new file mode 100644 index 00000000000..a0a24cbe18e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerA.ts @@ -0,0 +1,62 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Counter, CounterLive } from "./object.ts"; + +// Tag +export class WorkerA extends Cloudflare.Worker()( + "WorkerA", + { + main: import.meta.filename, + }, +) {} + +// Layer +export default WorkerA.make( + Effect.gen(function* () { + const counter = yield* Counter; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const key = request.headers["x-counter-key"] ?? "default"; + const stub = counter.getByName(key); + const url = new URL(request.url, "http://x"); + + if (request.method === "POST" && url.pathname === "/reset") { + yield* stub.reset(key); + return yield* HttpServerResponse.json({ ok: true }); + } + + if (request.method === "POST" && url.pathname === "/d1/increment") { + const value = yield* stub.incrementD1(key); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/d1") { + const value = yield* stub.getD1(key); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "POST" && url.pathname === "/do/increment") { + const value = yield* stub.incrementDO(); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/do") { + const value = yield* stub.getDO(); + return yield* HttpServerResponse.json({ value }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }), + }; + }).pipe( + Effect.provide( + // WorkerA needs to provide it + CounterLive.pipe(Layer.provide(Cloudflare.D1ConnectionLive)), + ), + ), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerB.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerB.ts new file mode 100644 index 00000000000..a96bed0f565 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerB.ts @@ -0,0 +1,53 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +import { Counter } from "./object.ts"; +import { WorkerA } from "./workerA.ts"; + +export default class WorkerB extends Cloudflare.Worker()( + "WorkerB", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const counter = yield* Counter.from(WorkerA); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const key = request.headers["x-counter-key"] ?? "default"; + const stub = counter.getByName(key); + const url = new URL(request.url, "http://x"); + + if (request.method === "POST" && url.pathname === "/reset") { + yield* stub.reset(key); + return yield* HttpServerResponse.json({ ok: true }); + } + + if (request.method === "POST" && url.pathname === "/d1/increment") { + const value = yield* stub.incrementD1(key); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/d1") { + const value = yield* stub.getD1(key); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "POST" && url.pathname === "/do/increment") { + const value = yield* stub.incrementDO(); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/do") { + const value = yield* stub.getDO(); + return yield* HttpServerResponse.json({ value }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerC.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerC.ts new file mode 100644 index 00000000000..db0af8d2c46 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-do/workerC.ts @@ -0,0 +1,56 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Counter, CounterLive } from "./object.ts"; + +// Tag — WorkerC also hosts its OWN Counter (declared in its public contract). +// Because each Worker hosts its own DO namespace, the instances under +// WorkerC are isolated from the instances under WorkerA/B. +export class WorkerC extends Cloudflare.Worker()( + "WorkerC", + { + main: import.meta.filename, + }, +) {} + +// Layer — uses `Counter.from(WorkerC)` (self-reference) instead of +// `yield* Counter`. The two forms are equivalent inside the host; the +// `.from(Self)` form is the recommended style for code that may be +// extracted into a reusable Layer. +export default WorkerC.make( + Effect.gen(function* () { + const counter = yield* Counter.from(WorkerC); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const key = request.headers["x-counter-key"] ?? "default"; + const stub = counter.getByName(key); + const url = new URL(request.url, "http://x"); + + if (request.method === "POST" && url.pathname === "/reset") { + yield* stub.reset(key); + return yield* HttpServerResponse.json({ ok: true }); + } + + if (request.method === "POST" && url.pathname === "/do/increment") { + const value = yield* stub.incrementDO(); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/do") { + const value = yield* stub.getDO(); + return yield* HttpServerResponse.json({ value }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }), + }; + }).pipe( + Effect.provide( + CounterLive.pipe(Layer.provide(Cloudflare.D1ConnectionLive)), + ), + ), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/group.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/group.ts new file mode 100644 index 00000000000..9cde3e6560c --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/group.ts @@ -0,0 +1,41 @@ +import * as Schema from "effect/Schema"; +import * as Rpc from "effect/unstable/rpc/Rpc"; +import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; + +/** + * Shared RPC group for the cross-script `tagged-rpc-do` fixture. + * + * Served on **two** ends: + * - Each {@link Cloudflare.RpcDurableObjectNamespace} instance + * (`Counter`) — the per-instance counter surface. + * - WorkerA, an {@link Cloudflare.RpcWorker} that forwards each call + * to `counter.getByName(key)` so consumers can hit the counter over + * a service binding without knowing about DO routing. + * + * Every payload carries `key`. WorkerA uses it to pick the DO instance + * via `getByName(key)`; the DO uses it for D1 row partitioning and + * ignores it for the per-instance storage methods (already isolated + * by `getByName(name)`). + */ +export class CounterRpcs extends RpcGroup.make( + Rpc.make("incrementD1", { + payload: { key: Schema.String }, + success: Schema.Struct({ value: Schema.Number }), + }), + Rpc.make("getD1", { + payload: { key: Schema.String }, + success: Schema.Struct({ value: Schema.Number }), + }), + Rpc.make("incrementDO", { + payload: { key: Schema.String }, + success: Schema.Struct({ value: Schema.Number }), + }), + Rpc.make("getDO", { + payload: { key: Schema.String }, + success: Schema.Struct({ value: Schema.Number }), + }), + Rpc.make("reset", { + payload: { key: Schema.String }, + success: Schema.Void, + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/object.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/object.ts new file mode 100644 index 00000000000..d4efafd93bb --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/object.ts @@ -0,0 +1,88 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { CounterRpcs } from "./group.ts"; + +export const MyDB = Cloudflare.D1Database("MyDB"); + +const DO_COUNT_KEY = "do_count"; + +// Tag — modular `RpcDurableObjectNamespace` declaration. The runtime +// (D1 setup, handler bodies) lives in `CounterLive` below; consumers +// only need this class identifier to bind via `Counter.from(WorkerA)`. +export class Counter extends Cloudflare.RpcDurableObjectNamespace()( + "Counter", + { schema: CounterRpcs }, +) {} + +// Layer — outer Effect resolves shared deps (D1), inner Effect runs +// once per instance and returns the piped `RpcServer.toHttpEffect` +// the DO's `fetch` handler will serve. +export const CounterLive = Counter.make( + Effect.gen(function* () { + const db = yield* Cloudflare.D1Connection.bind(MyDB); + + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + // D1's `exec()` splits on newlines and rejects multi-line statements; + // keep the DDL on a single line. + yield* db.exec( + "CREATE TABLE IF NOT EXISTS d1_counters (id TEXT PRIMARY KEY, value INTEGER NOT NULL DEFAULT 0)", + ); + + const readD1 = (key: string) => + db + .prepare("SELECT value FROM d1_counters WHERE id = ?") + .bind(key) + .first<{ value: number }>() + .pipe(Effect.map((row) => row?.value ?? 0)); + + const writeD1 = (key: string, value: number) => + db + .prepare( + "INSERT INTO d1_counters (id, value) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value", + ) + .bind(key, value) + .run() + .pipe(Effect.asVoid); + + const readDO = () => + state.storage.get(DO_COUNT_KEY).pipe(Effect.map((v) => v ?? 0)); + + const writeDO = (value: number) => + state.storage.put(DO_COUNT_KEY, value).pipe(Effect.asVoid); + + const handlers = CounterRpcs.toLayer({ + incrementD1: ({ key }) => + Effect.gen(function* () { + const next = (yield* readD1(key)) + 1; + yield* writeD1(key, next); + return { value: next }; + }), + getD1: ({ key }) => Effect.map(readD1(key), (value) => ({ value })), + // `key` is ignored — the DO instance is already partitioned by + // `getByName(key)` at the namespace boundary. + incrementDO: () => + Effect.gen(function* () { + const next = (yield* readDO()) + 1; + yield* writeDO(next); + return { value: next }; + }), + getDO: () => Effect.map(readDO(), (value) => ({ value })), + reset: ({ key }) => + Effect.gen(function* () { + console.log("reset DO", key); + yield* writeD1(key, 0); + yield* writeDO(0); + }), + }); + + return RpcServer.toHttpEffect(CounterRpcs).pipe( + Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), + ); + }); + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/stack.ts new file mode 100644 index 00000000000..129183e25cc --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/stack.ts @@ -0,0 +1,25 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import WorkerALayer, { WorkerA } from "./workerA.ts"; +import WorkerB from "./workerB.ts"; +import WorkerCLayer, { WorkerC } from "./workerC.ts"; + +export default Alchemy.Stack( + "TaggedRpcDOExample", + { + state: Cloudflare.state(), + providers: Cloudflare.providers(), + }, + Effect.gen(function* () { + const workerA = yield* WorkerA; + const workerB = yield* WorkerB; + const workerC = yield* WorkerC; + + return { + urlA: workerA.url.as(), + urlB: workerB.url.as(), + urlC: workerC.url.as(), + }; + }).pipe(Effect.provide(WorkerALayer), Effect.provide(WorkerCLayer)), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerA.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerA.ts new file mode 100644 index 00000000000..425fd8adc79 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerA.ts @@ -0,0 +1,72 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"; +import * as RpcServer from "effect/unstable/rpc/RpcServer"; +import { CounterRpcs } from "./group.ts"; +import { Counter, CounterLive } from "./object.ts"; + +// Tag — host worker for the typed `Counter` rpc DO. The third type +// argument declares `Counter` as part of WorkerA's public contract so +// that `Counter.from(WorkerA)` type-checks from any consumer. +// +// WorkerA's own `fetch` surface is the same `CounterRpcs` group as +// the DO — every call is proxied to `counter.getByName(key).method({...})`. +// Consumers can therefore hit the counter over a service binding via +// `Cloudflare.RpcWorker.bind(WorkerA)` without knowing about DO routing. +export class WorkerA extends Cloudflare.RpcWorker()( + "WorkerA", + { + main: import.meta.filename, + schema: CounterRpcs, + }, +) {} + +// Layer — yielding `Counter` resolves to WorkerA's local hosted +// namespace (the `CounterLive` Layer below populates the tag). +export default WorkerA.make( + Effect.gen(function* () { + const counter = yield* Counter; + + // `counter.getByName(key)` returns a chainable Proxy (built via + // `proxyChain`) so each method call yields directly. `Effect.orDie` + // drops the transport-level `RpcClientError`, which isn't part of + // the declared `CounterRpcs` error channel. + const handlers = CounterRpcs.toLayer({ + incrementD1: ({ key }) => + counter + .getByName(key) + .pipe(Effect.flatMap((c) => c.incrementD1({ key }))) + .pipe(Effect.orDie), + getD1: ({ key }) => + counter + .getByName(key) + .pipe(Effect.flatMap((c) => c.getD1({ key }))) + .pipe(Effect.orDie), + incrementDO: ({ key }) => + counter + .getByName(key) + .pipe(Effect.flatMap((c) => c.incrementDO({ key }))) + .pipe(Effect.orDie), + getDO: ({ key }) => + counter + .getByName(key) + .pipe(Effect.flatMap((c) => c.getDO({ key }))) + .pipe(Effect.orDie), + reset: ({ key }) => + counter + .getByName(key) + .pipe(Effect.flatMap((c) => c.reset({ key }))) + .pipe(Effect.orDie), + }); + + return RpcServer.toHttpEffect(CounterRpcs).pipe( + Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), + ); + }).pipe( + Effect.provide( + // WorkerA hosts `Counter`, so it must provide `CounterLive`. + CounterLive.pipe(Layer.provide(Cloudflare.D1ConnectionLive)), + ), + ), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerB.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerB.ts new file mode 100644 index 00000000000..996cb70dc3d --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerB.ts @@ -0,0 +1,57 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +import { Counter } from "./object.ts"; +import { WorkerA } from "./workerA.ts"; + +// Consumer worker — binds to WorkerA's hosted `Counter` rpc DO via +// `Counter.from(WorkerA)` (cross-script DO binding). Plain +// `Cloudflare.Worker` so we can drive the test through HTTP routes +// without needing an RpcClient setup for this end. +export default class WorkerB extends Cloudflare.Worker()( + "WorkerB", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const counter = yield* Counter.from(WorkerA); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const key = request.headers["x-counter-key"] ?? "default"; + const stub = yield* counter.getByName(key); + const url = new URL(request.url, "http://x"); + + if (request.method === "POST" && url.pathname === "/reset") { + yield* stub.reset({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ ok: true }); + } + + if (request.method === "POST" && url.pathname === "/d1/increment") { + const { value } = yield* stub.incrementD1({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/d1") { + const { value } = yield* stub.getD1({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "POST" && url.pathname === "/do/increment") { + const { value } = yield* stub.incrementDO({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/do") { + const { value } = yield* stub.getDO({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ value }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }).pipe(Effect.scoped), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerC.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerC.ts new file mode 100644 index 00000000000..dc8f4343c85 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/tagged-rpc-do/workerC.ts @@ -0,0 +1,57 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Counter, CounterLive } from "./object.ts"; + +// Tag — WorkerC also hosts its OWN `Counter` rpc DO (declared in its +// public contract). Each Worker host gets an isolated DO namespace, +// so writes via WorkerC's instances are NOT visible from WorkerA/B's +// instances of the same `Counter` class. +export class WorkerC extends Cloudflare.Worker()( + "WorkerC", + { + main: import.meta.filename, + }, +) {} + +// Layer — uses `Counter.from(WorkerC)` (self-reference) instead of +// the bare `yield* Counter`. Inside the host they're equivalent; the +// `.from(Self)` form is the recommended style for code that may be +// extracted into a reusable Layer. +export default WorkerC.make( + Effect.gen(function* () { + const counter = yield* Counter.from(WorkerC); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const key = request.headers["x-counter-key"] ?? "default"; + const stub = yield* counter.getByName(key); + const url = new URL(request.url, "http://x"); + + if (request.method === "POST" && url.pathname === "/reset") { + yield* stub.reset({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ ok: true }); + } + + if (request.method === "POST" && url.pathname === "/do/increment") { + const { value } = yield* stub.incrementDO({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ value }); + } + + if (request.method === "GET" && url.pathname === "/do") { + const { value } = yield* stub.getDO({ key }).pipe(Effect.orDie); + return yield* HttpServerResponse.json({ value }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }).pipe(Effect.scoped), + }; + }).pipe( + Effect.provide( + CounterLive.pipe(Layer.provide(Cloudflare.D1ConnectionLive)), + ), + ), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-async-caller.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-async-caller.ts new file mode 100644 index 00000000000..cbe894ef79c --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-async-caller.ts @@ -0,0 +1,33 @@ +/// + +/** + * Plain (non-Effect) Cloudflare Worker that calls a service-binding RPC + * method (`env.TARGET.greet(name)`) the same way any normal Cloudflare + * Worker would. + * + * GET /?name=foo → responds with whatever the target returns ("hello foo"), + * surfacing any error as a 500 with the message in the body so the test can + * assert against it directly instead of spelunking worker logs. + */ +export default { + async fetch( + request: Request, + env: { + TARGET: Service & { + greet: (name: string) => Promise; + }; + }, + ): Promise { + const name = new URL(request.url).searchParams.get("name") ?? "world"; + try { + console.log("async caller calling target"); + const greeting = await env.TARGET.greet(name); + console.log("async caller got greeting", greeting); + return new Response(String(greeting)); + } catch (err) { + console.log("async caller failed", err); + const message = err instanceof Error ? err.message : String(err); + return new Response(`async caller failed: ${message}`, { status: 500 }); + } + }, +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-effect-caller.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-effect-caller.ts new file mode 100644 index 00000000000..1ef869831ec --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-effect-caller.ts @@ -0,0 +1,41 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import BindingTargetWorker from "./binding-target-worker.ts"; + +/** + * Effect-native worker that calls another Effect-native worker's RPC method + * via `Cloudflare.bindWorker`. This is the canonical Effect → Effect cross- + * worker pattern: the bound stub returns Effects, errors and streams flow + * back through the typed channel. + * + * GET /?name=foo → pipes the bound `greet(name)` Effect through. + */ +export default class BindingEffectCaller extends Cloudflare.Worker()( + "BindingEffectCaller", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const target = yield* Cloudflare.bindWorker(BindingTargetWorker); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const name = + new URL(request.url, "http://x").searchParams.get("name") ?? "world"; + const greeting = yield* target.greet(name); + return HttpServerResponse.text(String(greeting)); + }).pipe( + Effect.catchCause((cause) => + Effect.succeed( + HttpServerResponse.text(`effect caller failed: ${String(cause)}`, { + status: 500, + }), + ), + ), + ), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-target-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-target-worker.ts new file mode 100644 index 00000000000..4a840be7201 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/binding-target-worker.ts @@ -0,0 +1,22 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +/** + * Effect-native worker that exposes a single RPC method (`greet`) plus a + * `fetch` handler. Used as the *callee* in the binding fixtures below. + */ +export default class BindingTargetWorker extends Cloudflare.Worker()( + "BindingTargetWorker", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + return { + greet: (name: string) => Effect.succeed(`hello ${name}`), + fetch: Effect.gen(function* () { + return HttpServerResponse.text("hello from BindingTargetWorker"); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/stack.ts new file mode 100644 index 00000000000..f3a528e5426 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker-worker-binding/stack.ts @@ -0,0 +1,48 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as pathe from "pathe"; +import BindingEffectCaller from "./binding-effect-caller.ts"; +import BindingTargetWorker from "./binding-target-worker.ts"; + +const asyncCallerMain = pathe.resolve( + import.meta.dirname, + "binding-async-caller.ts", +); + +/** + * Stack with three workers: + * + * - `BindingTargetWorker` — Effect-native target exposing `greet` (RPC) + + * `fetch`. + * - `BindingAsyncCaller` — plain `{ fetch }` Cloudflare worker that calls + * `env.TARGET.greet(name)` over a service binding. + * - `BindingEffectCaller` — Effect-native worker that uses + * `Cloudflare.bindWorker(BindingTargetWorker)` to call `greet` from + * inside an Effect. + */ +export default Alchemy.Stack( + "WorkerBindingStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const target = yield* BindingTargetWorker; + + const asyncCaller = yield* Cloudflare.Worker("BindingAsyncCaller", { + main: asyncCallerMain, + env: { + TARGET: target, + }, + }); + + const effectCaller = yield* BindingEffectCaller; + + return { + targetUrl: target.url.as(), + asyncCallerUrl: asyncCaller.url.as(), + effectCallerUrl: effectCaller.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker.ts new file mode 100644 index 00000000000..ea6c1f5a243 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/worker.ts @@ -0,0 +1,6 @@ +export default { + fetch: async () => { + return new Response("Hello from TestWorker"); + }, + queue: async () => {}, +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/stack.ts new file mode 100644 index 00000000000..c4c208f62c0 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/stack.ts @@ -0,0 +1,18 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import WorkflowTestWorker from "./workflow-worker.ts"; + +export default Alchemy.Stack( + "WorkflowBindingStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* WorkflowTestWorker; + return { + url: worker.url.as(), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/test-workflow.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/test-workflow.ts new file mode 100644 index 00000000000..7b419623a1b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/test-workflow.ts @@ -0,0 +1,38 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +/** + * Fixture workflow used by `Workflow.test.ts`. + * + * Exercises: + * - `Cloudflare.task` durable steps (greet + finalize) + * - `Cloudflare.sleep` between steps + * - `Cloudflare.WorkerEnvironment` access from inside the body — regression + * guard for https://github.com/alchemy-run/alchemy-effect/pull/71 + */ +export default class TestWorkflow extends Cloudflare.Workflow()( + "TestWorkflow", + Effect.gen(function* () { + return Effect.fn(function* (input: { value: string }) { + console.log("greeted"); + const env = yield* Cloudflare.WorkerEnvironment; + + const greeted = yield* Cloudflare.task( + "greet", + Effect.succeed(`Hello, ${input.value}!`), + ); + + yield* Cloudflare.sleep("cooldown", "1 second"); + + const finalized = yield* Cloudflare.task( + "finalize", + Effect.succeed({ + greeting: greeted, + envBindingCount: Object.keys(env).length, + }), + ); + + return finalized; + }); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/workflow-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/workflow-worker.ts new file mode 100644 index 00000000000..e5dbfc204f4 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Workers/fixtures/workflow/workflow-worker.ts @@ -0,0 +1,38 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import TestWorkflow from "./test-workflow.ts"; + +export default class WorkflowTestWorker extends Cloudflare.Worker()( + "WorkflowTestWorker", + { + main: import.meta.filename, + subdomain: { enabled: true, previewsEnabled: false }, + compatibility: { date: "2024-09-23", flags: ["nodejs_compat"] }, + }, + Effect.gen(function* () { + const workflow = yield* TestWorkflow; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + + if (request.url.startsWith("/workflow/start/")) { + const value = request.url.split("/workflow/start/")[1] ?? "world"; + const instance = yield* workflow.create({ value }); + return yield* HttpServerResponse.json({ instanceId: instance.id }); + } + + if (request.url.startsWith("/workflow/status/")) { + const instanceId = request.url.split("/workflow/status/")[1] ?? ""; + const instance = yield* workflow.get(instanceId); + const status = yield* instance.status(); + return yield* HttpServerResponse.json(status); + } + + return HttpServerResponse.text("ok"); + }), + }; + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zaraz/ZarazConfig.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zaraz/ZarazConfig.test.ts new file mode 100644 index 00000000000..c5399df0063 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zaraz/ZarazConfig.test.ts @@ -0,0 +1,175 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { stripNullFields, stripUndefinedFields } from "@/Util/data"; +import * as zaraz from "@distilled.cloud/cloudflare/zaraz"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; + +const { test } = Test.make({ providers: Cloudflare.providers() }); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const zoneId = process.env.CLOUDFLARE_TEST_ZARAZ_ZONE_ID; +const zoneName = + process.env.CLOUDFLARE_TEST_ZARAZ_ZONE_NAME ?? "alchemy-test-2.us"; + +test.provider.skipIf(!zoneId)( + "updates and retains a zone-level Zaraz config", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const original = yield* zaraz.getConfig({ zoneId: zoneId! }); + const toggledDataLayer = !original.dataLayer; + + yield* Effect.gen(function* () { + const updated = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.ZarazConfig("Config", { + zone: { zoneId: zoneId!, name: zoneName }, + dataLayer: toggledDataLayer, + }); + }), + ); + + expect(updated.zoneId).toEqual(zoneId); + expect(updated.dataLayer).toEqual(toggledDataLayer); + + const liveUpdated = yield* zaraz.getConfig({ zoneId: zoneId! }); + expect(liveUpdated.dataLayer).toEqual(toggledDataLayer); + + const restored = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.ZarazConfig("Config", { + zone: { zoneId: zoneId!, name: zoneName }, + dataLayer: original.dataLayer, + }); + }), + ); + + expect(restored.dataLayer).toEqual(original.dataLayer); + + yield* stack.destroy(); + + const liveRetained = yield* zaraz.getConfig({ zoneId: zoneId! }); + expect(liveRetained.dataLayer).toEqual(original.dataLayer); + }).pipe( + Effect.ensuring( + zaraz.putConfig(toPutConfig(zoneId!, original)).pipe(Effect.ignore), + ), + ); + }).pipe(logLevel), + { timeout: 120_000 }, +); + +test.provider.skipIf(!zoneId)( + "delete true resets Zaraz config to defaults", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const original = yield* zaraz.getConfig({ zoneId: zoneId! }); + const originalWorkflow = yield* zaraz.getWorkflow({ zoneId: zoneId! }); + const defaults = yield* zaraz.getDefault({ zoneId: zoneId! }); + + yield* Effect.gen(function* () { + yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.ZarazConfig("Config", { + zone: { zoneId: zoneId!, name: zoneName }, + dataLayer: !defaults.dataLayer, + workflow: "preview", + delete: true, + }); + }), + ); + + const liveUpdated = yield* zaraz.getConfig({ zoneId: zoneId! }); + expect(liveUpdated.dataLayer).toEqual(!defaults.dataLayer); + + yield* stack.destroy(); + + const liveDeleted = yield* zaraz.getConfig({ zoneId: zoneId! }); + expect(liveDeleted.dataLayer).toEqual(defaults.dataLayer); + const liveDeletedWorkflow = yield* zaraz.getWorkflow({ + zoneId: zoneId!, + }); + expect(liveDeletedWorkflow).toEqual("realtime"); + }).pipe( + Effect.ensuring( + Effect.gen(function* () { + yield* zaraz + .putConfig(toPutConfig(zoneId!, original)) + .pipe(Effect.ignore); + yield* zaraz + .putZaraz({ zoneId: zoneId!, workflow: originalWorkflow }) + .pipe(Effect.ignore); + }), + ), + ); + }).pipe(logLevel), + { timeout: 120_000 }, +); + +test.provider.skipIf(!zoneId)( + "updates and retains Zaraz workflow mode", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const original = yield* zaraz.getWorkflow({ zoneId: zoneId! }); + const workflow = original === "realtime" ? "preview" : "realtime"; + + yield* Effect.gen(function* () { + const updated = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.ZarazConfig("Config", { + zone: { zoneId: zoneId!, name: zoneName }, + workflow, + }); + }), + ); + + expect(updated.workflow).toEqual(workflow); + + const liveUpdated = yield* zaraz.getWorkflow({ zoneId: zoneId! }); + expect(liveUpdated).toEqual(workflow); + + yield* stack.destroy(); + + const liveRetained = yield* zaraz.getWorkflow({ zoneId: zoneId! }); + expect(liveRetained).toEqual(workflow); + }).pipe( + Effect.ensuring( + zaraz + .putZaraz({ zoneId: zoneId!, workflow: original }) + .pipe(Effect.ignore), + ), + ); + }).pipe(logLevel), + { timeout: 120_000 }, +); + +type ConfigResponse = zaraz.GetConfigResponse | zaraz.PutConfigResponse; + +const toPutConfig = ( + zoneId: string, + config: ConfigResponse, +): zaraz.PutConfigRequest => + stripUndefinedFields({ + zoneId, + dataLayer: config.dataLayer, + debugKey: config.debugKey, + settings: stripNullFields(config.settings), + tools: config.tools, + triggers: config.triggers, + variables: config.variables, + zarazVersion: config.zarazVersion, + analytics: config.analytics ? stripNullFields(config.analytics) : undefined, + consent: config.consent ? stripNullFields(config.consent) : undefined, + historyChange: config.historyChange ?? undefined, + }) as zaraz.PutConfigRequest; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zaraz/ZarazEventTypes.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zaraz/ZarazEventTypes.test.ts new file mode 100644 index 00000000000..b6de8a54078 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zaraz/ZarazEventTypes.test.ts @@ -0,0 +1,153 @@ +import { + ZarazConfig, + type InferZarazEcommerceEvents, + type InferZarazEvents, + type ZarazHttpEventsPayload, + type ZarazTrack, + type ZarazWebApi, +} from "@/Cloudflare/Zaraz"; +import { expect, test } from "vitest"; + +const checkZarazEventTypes = () => { + const zaraz = ZarazConfig.events<{ + Login: { method: "google" | "email" | "email-link" }; + "Button Clicked": { button_label: string; context?: string }; + __zarazSPA: undefined; + }>(); + + const zarazWithEcommerce = ZarazConfig.events<{ + Login: { method: "google" | "email" | "email-link" }; + "Button Clicked": { button_label: string; context?: string }; + __zarazSPA: undefined; + }>({ ecommerce: true }); + + type Events = InferZarazEvents; + type DisabledEcommerceEvents = InferZarazEcommerceEvents; + type EcommerceEvents = InferZarazEcommerceEvents; + + const standardEcommerceEvents = { + "Product List Viewed": true, + "Products Searched": true, + "Product Clicked": true, + "Product Added": true, + "Product Added to Wishlist": true, + "Product Removed": true, + "Product Viewed": true, + "Cart Viewed": true, + "Checkout Started": true, + "Checkout Step Viewed": true, + "Checkout Step Completed": true, + "Payment Info Entered": true, + "Order Completed": true, + "Order Updated": true, + "Order Refunded": true, + "Order Cancelled": true, + "Clicked Promotion": true, + "Viewed Promotion": true, + "Shipping Info Entered": true, + } satisfies Record; + + void standardEcommerceEvents; + + const track = undefined as unknown as ZarazTrack; + + track("Login", { method: "google" }); + track("__zarazSPA"); + track("__zarazSPA", undefined); + track("Button Clicked", { button_label: "Save" }); + track("Button Clicked", { button_label: "Save", context: "toolbar" }); + + // @ts-expect-error event name must exist in the contract + track("Missing", {}); + + // @ts-expect-error event properties are checked per event name + track("Login", { method: "github" }); + + // @ts-expect-error events with undefined properties do not accept an object + track("__zarazSPA", {}); + + // @ts-expect-error required event properties must be present + track("Button Clicked", {}); + + const browser = undefined as unknown as ZarazWebApi; + const browserWithoutEcommerce = undefined as unknown as ZarazWebApi< + Events, + DisabledEcommerceEvents + >; + + browser.track("Login", { method: "email-link" }); + browser.ecommerce("Product Viewed", { product_id: "product-1", price: 12 }); + browser.ecommerce("Products Searched", { query: "shirts" }); + browser.ecommerce("Checkout Step Viewed", { step: 1 }); + browser.ecommerce("Order Completed", { + order_id: "order-1", + total: 24, + currency: "USD", + products: [{ product_id: "product-1", quantity: 2 }], + }); + + // @ts-expect-error ecommerce events are disabled unless the contract opts in + browserWithoutEcommerce.ecommerce("Product Viewed", { + product_id: "product-1", + }); + + // @ts-expect-error ecommerce events use the ecommerce contract + browser.ecommerce("Login", { method: "google" }); + + // @ts-expect-error ecommerce payloads are checked + browser.ecommerce("Product Viewed", { invalid: "product-1" }); + + // @ts-expect-error ecommerce events require at least one supported property + browser.ecommerce("Product Viewed", {}); + + const payload = { + events: [ + { + client: { + __zarazTrack: "Login", + method: "email", + }, + system: { + page: { + title: "Login", + url: "https://example.com/login", + }, + }, + }, + ], + } satisfies ZarazHttpEventsPayload; + + void payload; + + const emptyPayload = { + events: [ + { + client: { + __zarazTrack: "__zarazSPA", + }, + }, + ], + } satisfies ZarazHttpEventsPayload; + + void emptyPayload; + + const invalidPayload = { + events: [ + { + client: { + __zarazTrack: "Login", + // @ts-expect-error HTTP event payloads validate discriminated event properties + method: "github", + }, + }, + ], + } satisfies ZarazHttpEventsPayload; + + void invalidPayload; +}; + +void checkZarazEventTypes; + +test("Zaraz event contracts are type-only", () => { + expect(ZarazConfig.events<{}>()).toEqual({}); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zone/Zone.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zone/Zone.test.ts new file mode 100644 index 00000000000..9e5dc929f95 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Zone/Zone.test.ts @@ -0,0 +1,208 @@ +import { adopt, OwnedBySomeoneElse } from "@/AdoptPolicy"; +import * as Cloudflare from "@/Cloudflare"; +import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment"; +import { findZoneByName } from "@/Cloudflare/Zone/lookup"; +import { destroy } from "@/RemovalPolicy"; +import * as Test from "@/Test/Vitest"; +import * as zones from "@distilled.cloud/cloudflare/zones"; +import { expect } from "@effect/vitest"; +import * as Cause from "effect/Cause"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; + +const { test } = Test.make({ providers: Cloudflare.providers() }); + +// Cloudflare's POST /zones rejects reserved pseudo-TLDs (`.test`, `.local`, +// `.example`) with "unable to identify ... as a registered domain". A +// syntactically-valid, registerable name is accepted into a `pending` zone +// even when the domain isn't actually registered to us — which is all these +// create/delete lifecycle tests need. Derive the name from the test account id +// so it's deterministic and never collides with a real zone. +const zoneNameFor = (accountId: string, label: string) => + process.env.TEST_ZONE_NAME ?? `alchemy-${label}-${accountId}.com`; + +test.provider( + "create zone retains by default — destroy() opts in to deletion", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const TEST_ZONE = zoneNameFor(accountId, "destroy"); + + const zone = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Zone("CreatedZone", { + name: TEST_ZONE, + }).pipe(destroy()); + }), + ); + + expect(zone.name).toBe(TEST_ZONE); + expect(zone.accountId).toBe(accountId); + + // The mapped attributes should match what the Cloudflare API reports, + // with the API's `null`s normalized to `undefined`. + const live = yield* zones.getZone({ zoneId: zone.zoneId }); + expect(zone.zoneId).toBe(live.id); + expect(zone.name).toBe(live.name); + expect(zone.accountName).toBe(live.account.name ?? undefined); + expect(zone.type).toBe(live.type ?? "full"); + expect(zone.status).toBe(live.status ?? undefined); + expect(zone.paused).toBe(live.paused ?? false); + expect(zone.nameServers).toEqual(live.nameServers); + expect(zone.originalNameServers).toEqual( + live.originalNameServers ?? undefined, + ); + expect(zone.vanityNameServers).toEqual( + live.vanityNameServers ?? undefined, + ); + expect(zone.activatedOn).toBe(live.activatedOn ?? undefined); + expect(zone.createdOn).toBe(live.createdOn); + expect(zone.modifiedOn).toBe(live.modifiedOn); + expect(zone.developmentMode).toBe(live.developmentMode); + expect(zone.originalDnshost).toBe(live.originalDnshost ?? undefined); + expect(zone.originalRegistrar).toBe(live.originalRegistrar ?? undefined); + expect(zone.cnameSuffix).toBe(live.cnameSuffix ?? undefined); + expect(zone.verificationKey).toBe(live.verificationKey ?? undefined); + expect(zone.owner.id).toBe(live.owner.id ?? undefined); + expect(zone.meta.foundationDns).toBe( + live.meta.foundationDns ?? undefined, + ); + + yield* stack.destroy(); + + yield* waitForZoneToBeDeleted(zone.zoneId); + }), +); + +test.provider( + "create zone retains by default — survives stack.destroy()", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const TEST_ZONE = zoneNameFor(accountId, "retain"); + + const zone = yield* stack.deploy( + Effect.gen(function* () { + return yield* Cloudflare.Zone("RetainedZone", { + name: TEST_ZONE, + }); + }), + ); + + expect(zone.name).toBe(TEST_ZONE); + expect(zone.accountId).toBe(accountId); + + yield* stack.destroy(); + + const live = yield* zones.getZone({ zoneId: zone.zoneId }); + expect(live.id).toBe(zone.zoneId); + + // clean up the retained zone so the test is repeatable + yield* zones.deleteZone({ zoneId: zone.zoneId }); + yield* waitForZoneToBeDeleted(zone.zoneId); + }), +); + +test.provider( + "adoption — existing zone errors without adopt, takes over with adopt(true)", + (stack) => + Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + const TEST_ZONE = zoneNameFor(accountId, "adopt"); + + // Create the zone out-of-band so the stack has no state of its own for + // it — exactly the "the zone already exists" scenario. Tolerate a zone + // left behind by an interrupted run so the test stays repeatable. + const existing = yield* zones + .createZone({ + account: { id: accountId }, + name: TEST_ZONE, + type: "full", + }) + .pipe( + Effect.catchTag("ZoneAlreadyExists", () => + findZoneByName({ accountId, name: TEST_ZONE }).pipe( + Effect.flatMap((match) => + match + ? Effect.succeed(match) + : Effect.die(new Error(`zone ${TEST_ZONE} not found`)), + ), + ), + ), + ); + + // Without `adopt`: a Cloudflare zone carries no ownership markers, so the + // engine cannot prove we created it and refuses to take it over. The + // engine surfaces this as a defect, so catch the whole cause and pull the + // typed error back out rather than string-matching the message. + const error = yield* stack + .deploy( + Effect.gen(function* () { + return yield* Cloudflare.Zone("AdoptedZone", { name: TEST_ZONE }); + }), + ) + .pipe( + Effect.as(undefined), + Effect.catchCause((cause) => Effect.succeed(findOwnedError(cause))), + ); + expect(error).toBeInstanceOf(OwnedBySomeoneElse); + + // With `adopt(true)`: the engine takes over the pre-existing zone instead + // of creating a new one. `destroy()` opts the adopted zone into deletion + // on teardown so the test stays repeatable. + const adopted = yield* stack + .deploy( + Effect.gen(function* () { + return yield* Cloudflare.Zone("AdoptedZone", { + name: TEST_ZONE, + }).pipe(destroy()); + }), + ) + .pipe(adopt(true)); + expect(adopted.zoneId).toBe(existing.id); + expect(adopted.name).toBe(TEST_ZONE); + + yield* stack.destroy(); + yield* waitForZoneToBeDeleted(existing.id); + }), +); + +const waitForZoneToBeDeleted = Effect.fn(function* (zoneId: string) { + yield* zones.getZone({ zoneId }).pipe( + // A successful read means the zone is still around — force a retry. + Effect.flatMap(() => new ZoneStillExists()), + // Any other failure (e.g. `Invalid zone identifier` / 404) means the zone + // is gone, which is exactly what we're waiting for. + Effect.catch((e) => + e instanceof ZoneStillExists ? Effect.fail(e) : Effect.void, + ), + Effect.retry({ + while: (e): e is ZoneStillExists => e instanceof ZoneStillExists, + schedule: Schedule.exponential(100), + times: 20, + }), + ); +}); + +class ZoneStillExists extends Data.TaggedError("ZoneStillExists") {} + +/** + * Pull the {@link OwnedBySomeoneElse} value out of a Cause regardless of + * whether the engine raised it as a typed failure or a defect. + */ +const findOwnedError = ( + cause: Cause.Cause, +): OwnedBySomeoneElse | undefined => + cause.reasons + .map((reason) => + Cause.isFailReason(reason) + ? reason.error + : Cause.isDieReason(reason) + ? reason.defect + : undefined, + ) + .find( + (value): value is OwnedBySomeoneElse => + value instanceof OwnedBySomeoneElse, + ); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Drizzle/Schema.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Drizzle/Schema.test.ts new file mode 100644 index 00000000000..c18cdd6fb64 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Drizzle/Schema.test.ts @@ -0,0 +1,106 @@ +import * as Drizzle from "@/Drizzle"; +import * as Stack from "@/Stack"; +import { State } from "@/State"; +import * as Test from "@/Test/Vitest"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; + +const { test } = Test.make({ providers: Drizzle.providers() }); + +// Minimal drizzle-orm schema source — enough for `generateDrizzleJson` +// to produce a non-empty snapshot. +const SCHEMA_SOURCE = ` +import { pgTable, serial, text } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: serial("id").primaryKey(), + name: text("name").notNull(), +}); +`; + +const DRIFTED_SCHEMA_SOURCE = + SCHEMA_SOURCE + + `\nexport const posts = pgTable("posts", {\n id: serial("id").primaryKey(),\n title: text("title").notNull(),\n});\n`; + +const stageWorkspace = (initialSource: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectory({ + prefix: "alchemy-drizzle-schema-test-", + }); + const schemaPath = path.join(root, "schema.ts"); + yield* fs.writeFileString(schemaPath, initialSource); + const out = path.join(root, "migrations"); + return { root, out, schemaPath }; + }); + +const getStatus = Effect.fn(function* (fqn: string) { + const state = yield* yield* State; + const stk = yield* Stack.Stack; + const s = yield* state.get({ stack: stk.name, stage: stk.stage, fqn }); + return s?.status; +}); + +test.provider( + "repeated deploys with no schema drift produce noop, not update (regression: forced update cascaded into Neon.Branch)", + (stack) => + Effect.gen(function* () { + const ws = yield* stageWorkspace(SCHEMA_SOURCE); + + yield* stack.deploy( + Drizzle.Schema("app-schema", { + schema: ws.schemaPath, + out: ws.out, + }), + ); + expect(yield* getStatus("app-schema")).toEqual("created"); + + // Second deploy with the same schema. Before the fix, Schema.diff + // returned `{ action: "update" }` unconditionally, so status would + // flip to "updated" and downstream resources (e.g. Neon.Branch) + // would see `schema.out` as an unresolved Output during plan and + // cascade into their own spurious updates. + yield* stack.deploy( + Drizzle.Schema("app-schema", { + schema: ws.schemaPath, + out: ws.out, + }), + ); + expect(yield* getStatus("app-schema")).toEqual("created"); + }), +); + +test.provider( + "deploy after a real schema change updates the resource", + (stack) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const ws = yield* stageWorkspace(SCHEMA_SOURCE); + + const initial = yield* stack.deploy( + Drizzle.Schema("app-schema", { + schema: ws.schemaPath, + out: ws.out, + }), + ); + + // Write the drifted schema as a *new* file so the dynamic import + // cache doesn't hand us the original module. + const driftedSchemaPath = path.join(ws.root, "schema-drifted.ts"); + yield* fs.writeFileString(driftedSchemaPath, DRIFTED_SCHEMA_SOURCE); + + const drifted = yield* stack.deploy( + Drizzle.Schema("app-schema", { + schema: driftedSchemaPath, + out: ws.out, + }), + ); + + expect(yield* getStatus("app-schema")).toEqual("updated"); + expect(drifted.snapshotHash).not.toEqual(initial.snapshotHash); + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/FQN.test.ts b/.repos/alchemy-effect/packages/alchemy/test/FQN.test.ts new file mode 100644 index 00000000000..2bdb8cda228 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/FQN.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "@effect/vitest"; +import { fromPath, FQN_SEPARATOR, parseFqn, toFqn, toPath } from "../src/FQN"; +import type { NamespaceNode } from "../src/Namespace"; + +describe("FQN", () => { + describe("toPath", () => { + test("returns empty array for undefined namespace", () => { + expect(toPath(undefined)).toEqual([]); + }); + + test("returns single element for root namespace", () => { + const ns: NamespaceNode = { Id: "Root" }; + expect(toPath(ns)).toEqual(["Root"]); + }); + + test("returns path from root to leaf", () => { + const ns: NamespaceNode = { + Id: "Child", + Parent: { + Id: "Parent", + Parent: { Id: "Root" }, + }, + }; + expect(toPath(ns)).toEqual(["Root", "Parent", "Child"]); + }); + }); + + describe("toFqn", () => { + test("returns logicalId for undefined namespace", () => { + expect(toFqn(undefined, "MyResource")).toBe("MyResource"); + }); + + test("returns namespace-qualified name", () => { + const ns: NamespaceNode = { Id: "Parent" }; + expect(toFqn(ns, "MyResource")).toBe(`Parent${FQN_SEPARATOR}MyResource`); + }); + + test("handles deep namespace", () => { + const ns: NamespaceNode = { + Id: "Child", + Parent: { Id: "Parent" }, + }; + expect(toFqn(ns, "MyResource")).toBe( + `Parent${FQN_SEPARATOR}Child${FQN_SEPARATOR}MyResource`, + ); + }); + }); + + describe("parseFqn", () => { + test("parses simple logicalId", () => { + expect(parseFqn("MyResource")).toEqual({ + path: [], + logicalId: "MyResource", + }); + }); + + test("parses namespaced FQN", () => { + expect(parseFqn("Parent/Child/MyResource")).toEqual({ + path: ["Parent", "Child"], + logicalId: "MyResource", + }); + }); + + test("parses single namespace FQN", () => { + expect(parseFqn("Parent/MyResource")).toEqual({ + path: ["Parent"], + logicalId: "MyResource", + }); + }); + }); + + describe("fromPath", () => { + test("returns undefined for empty path", () => { + expect(fromPath([])).toBeUndefined(); + }); + + test("returns single node for single element", () => { + const result = fromPath(["Root"]); + expect(result).toEqual({ Id: "Root", Parent: undefined }); + }); + + test("returns nested nodes", () => { + const result = fromPath(["Root", "Parent", "Child"]); + expect(result).toEqual({ + Id: "Child", + Parent: { + Id: "Parent", + Parent: { Id: "Root", Parent: undefined }, + }, + }); + }); + }); + + describe("roundtrip", () => { + test("toFqn -> parseFqn -> fromPath -> toFqn", () => { + const ns: NamespaceNode = { + Id: "Child", + Parent: { Id: "Parent" }, + }; + const logicalId = "MyResource"; + const fqn = toFqn(ns, logicalId); + const parsed = parseFqn(fqn); + const reconstructedNs = fromPath(parsed.path); + const roundtripFqn = toFqn(reconstructedNs, parsed.logicalId); + expect(roundtripFqn).toBe(fqn); + }); + }); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/KeyPair.test.ts b/.repos/alchemy-effect/packages/alchemy/test/KeyPair.test.ts new file mode 100644 index 00000000000..caaf2c27270 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/KeyPair.test.ts @@ -0,0 +1,86 @@ +import * as NodeCrypto from "node:crypto"; +import { KeyPair, KeyPairProvider } from "@/KeyPair"; +import { inMemoryState } from "@/State"; +import * as Test from "@/Test/Vitest"; +import { describe, expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; + +const { test } = Test.make({ + providers: KeyPairProvider(), + state: inMemoryState(), +}); + +const assertPemKeyPair = (attrs: { + privateKey: Redacted.Redacted; + publicKey: string; +}) => { + const priv = Redacted.value(attrs.privateKey); + expect(priv).toMatch(/^-----BEGIN PRIVATE KEY-----/); + expect(priv).toMatch(/-----END PRIVATE KEY-----/); + expect(attrs.publicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/); + expect(attrs.publicKey).toMatch(/-----END PUBLIC KEY-----/); + // Node throws if the PEM is malformed. + NodeCrypto.createPrivateKey(priv); + NodeCrypto.createPublicKey(attrs.publicKey); +}; + +describe("Alchemy.KeyPair", () => { + test.provider("mints an ed25519 keypair by default", (stack) => + Effect.gen(function* () { + const attrs = yield* stack.deploy( + Effect.gen(function* () { + return yield* KeyPair("ed25519-default"); + }), + ); + expect(attrs.algorithm).toBe("ed25519"); + assertPemKeyPair(attrs); + }), + ); + + test.provider("mints an rsa keypair when requested", (stack) => + Effect.gen(function* () { + const attrs = yield* stack.deploy( + Effect.gen(function* () { + return yield* KeyPair("rsa-key", { + algorithm: "rsa", + modulusLength: 2048, + }); + }), + ); + expect(attrs.algorithm).toBe("rsa"); + assertPemKeyPair(attrs); + }), + ); + + test.provider("mints an ec keypair when requested", (stack) => + Effect.gen(function* () { + const attrs = yield* stack.deploy( + Effect.gen(function* () { + return yield* KeyPair("ec-key", { + algorithm: "ec", + namedCurve: "P-256", + }); + }), + ); + expect(attrs.algorithm).toBe("ec"); + assertPemKeyPair(attrs); + }), + ); + + test.provider("preserves the keypair across deploys", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + return yield* KeyPair("stable-key"); + }); + + const first = yield* stack.deploy(program); + const second = yield* stack.deploy(program); + + expect(Redacted.value(second.privateKey)).toBe( + Redacted.value(first.privateKey), + ); + expect(second.publicKey).toBe(first.publicKey); + }), + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/RpcProvider.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcProvider.test.ts new file mode 100644 index 00000000000..49e9ecd61b1 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcProvider.test.ts @@ -0,0 +1,179 @@ +import { AlchemyContext } from "@/AlchemyContext.ts"; +import { InstanceId } from "@/InstanceId.ts"; +import * as RpcProvider from "@/Local/RpcProvider.ts"; +import type { ProviderService } from "@/Provider.ts"; +import { Resource } from "@/Resource.ts"; +import { Stack, type StackSpec } from "@/Stack.ts"; +import { Stage } from "@/Stage.ts"; +import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; + +interface TestResource extends Resource< + "Local.RpcProvider.Test", + {}, + { ok: boolean } +> {} +const TestResource = Resource("Local.RpcProvider.Test"); + +type StackShape = Omit; + +interface Capture { + stack?: StackShape; + stage?: string; + instanceId?: string; +} + +const defaultStack: StackShape = { + name: "default-stack", + stage: "default-stage", + resources: {}, + bindings: {}, + actions: {}, +}; + +describe("Local.RpcProvider.effect", () => { + it.effect( + "provides default Stack, Stage, and InstanceId to lifecycle effects", + () => + Effect.gen(function* () { + const [capture, result] = yield* useProvider((provider) => + provider.reconcile({ + id: "r", + instanceId: "inst-from-arg", + news: {}, + olds: undefined, + output: undefined, + session: undefined as any, + bindings: [], + }), + ); + expect(result).toEqual({ ok: true }); + expect(capture.stack).toBe(defaultStack); + expect(capture.stage).toBe(defaultStack.stage); + expect(capture.instanceId).toBe("inst-from-arg"); + }), + ); + + it.effect( + "does not override Stack, Stage, or InstanceId when already provided", + () => + Effect.gen(function* () { + const overrideStack: StackShape = { + name: "override-stack", + stage: "ignored-stage-on-stack", + resources: {}, + bindings: {}, + actions: {}, + }; + const [capture] = yield* useProvider((provider) => + provider + .reconcile({ + id: "r", + instanceId: "inst-from-arg", + news: {}, + olds: undefined, + output: undefined, + session: undefined as any, + bindings: [], + }) + .pipe( + Effect.provideService(Stack, overrideStack), + Effect.provideService(Stage, "override-stage"), + Effect.provideService(InstanceId, "override-instance-id"), + ), + ); + expect(capture.stack).toBe(overrideStack); + expect(capture.stage).toBe("override-stage"); + expect(capture.instanceId).toBe("override-instance-id"); + }), + ); + + it.effect("provides defaults to Stream-returning lifecycle methods", () => + Effect.gen(function* () { + const [capture, items] = yield* useProvider((provider) => + provider.tail!({ + id: "r", + instanceId: "inst-from-arg", + props: {}, + output: { ok: true }, + }).pipe(Stream.runCollect), + ); + expect(items.length).toBe(1); + expect(capture.stack).toBe(defaultStack); + expect(capture.stage).toBe(defaultStack.stage); + expect(capture.instanceId).toBe("inst-from-arg"); + }), + ); + + it.effect("omits InstanceId fallback when input has no instanceId", () => + Effect.gen(function* () { + const [capture, exit] = yield* useProvider((provider) => + provider + .reconcile({ + id: "r", + news: {}, + olds: undefined, + output: undefined, + session: undefined!, + instanceId: undefined!, + bindings: [], + }) + .pipe(Effect.exit), + ); + expect(exit._tag).toBe("Failure"); + expect(capture.stack).toBe(defaultStack); + expect(capture.stage).toBe(defaultStack.stage); + expect(capture.instanceId).toBeUndefined(); + }), + ); +}); + +const TestResourceProvider = (capture: Capture) => + RpcProvider.effect( + TestResource, + "ignored://entry", + Effect.succeed({ + reconcile: Effect.fn(function* (_input: any) { + capture.stack = yield* Stack; + capture.stage = yield* Stage; + capture.instanceId = yield* InstanceId; + return { ok: true }; + }), + tail: (_input: any) => + Stream.fromEffect( + Effect.gen(function* () { + capture.stack = yield* Stack; + capture.stage = yield* Stage; + capture.instanceId = yield* InstanceId; + return { timestamp: new Date(0), message: "ok" }; + }), + ), + delete: Effect.fn(function* () {}), + }), + ); + +const useProvider = ( + callback: (provider: ProviderService) => Effect.Effect, +) => { + const capture: Capture = {}; + return TestResource.Provider.pipe( + Effect.flatMap(callback), + Effect.map((result) => [capture, result] as const), + Effect.provide( + Layer.provide( + TestResourceProvider(capture), + Layer.mergeAll( + Layer.succeed(Stack, defaultStack), + Layer.succeed(Stage, defaultStack.stage), + Layer.succeed(AlchemyContext, { + dotAlchemy: "/tmp/.alchemy", + dev: false, + adopt: false, + }), + ), + ), + ), + ); +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSerialization.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSerialization.test.ts new file mode 100644 index 00000000000..5c746b354d8 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSerialization.test.ts @@ -0,0 +1,228 @@ +import { + unwrapRpcHandlers, + wrapRpcHandlers, + type RpcWrapped, +} from "@/Local/RpcSerialization.ts"; +import * as Output from "@/Output.ts"; +import { describe, expect, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Redacted from "effect/Redacted"; +import * as Stream from "effect/Stream"; + +/** + * Builds a client whose wrap→unwrap path mirrors the production wire: + * `unwrapRpcEffectHandler` serializes its args; capnweb JSON-encodes them + * over the websocket; `wrapRpcEffectHandler` deserializes them on the other + * end. We thread `JSON.parse(JSON.stringify(...))` between the wrap and + * unwrap layers to model that hop. Streams skip the JSON hop because they + * are bridged via a real `ReadableStream`. + */ +const roundTrip = >( + handlers: T, + streamKeys?: Array, +): T => { + const wrapped = wrapRpcHandlers(handlers, streamKeys); + const piped = Object.fromEntries( + Object.entries(wrapped).map(([key, value]) => { + if (typeof value !== "function") { + return [key, value]; + } + if (streamKeys?.includes(key as keyof T)) { + // Streams bypass JSON; the underlying ReadableStream is shipped. + return [key, value]; + } + const fn = value as (args: Array) => Promise; + const piped = async (args: Array) => { + const wireArgs = JSON.parse(JSON.stringify(args)); + const result = await fn(wireArgs); + return JSON.parse(JSON.stringify(result)); + }; + return [key, piped]; + }), + ) as RpcWrapped; + return unwrapRpcHandlers(piped, streamKeys) as T; +}; + +describe("Local.RpcSerialization", () => { + describe("argument serialization", () => { + it.effect("round-trips a top-level Redacted argument", () => + Effect.gen(function* () { + const handlers = { + echo: (s: Redacted.Redacted) => + Effect.succeed(Redacted.value(s)), + }; + const client = roundTrip(handlers); + expect(yield* client.echo(Redacted.make("hush"))).toBe("hush"); + }), + ); + + it.effect("round-trips a Redacted nested inside an object", () => + Effect.gen(function* () { + const handlers = { + password: (env: { password: Redacted.Redacted }) => + Effect.succeed(Redacted.value(env.password)), + }; + const client = roundTrip(handlers); + expect( + yield* client.password({ password: Redacted.make("hush") }), + ).toBe("hush"); + }), + ); + + it.effect("preserves objects with their own toJSON (Date)", () => + Effect.gen(function* () { + const handlers = { + withDate: (env: { when: Date | string }) => + Effect.succeed(String(env.when)), + }; + const client = roundTrip(handlers); + const d = new Date("2026-05-16T12:00:00.000Z"); + expect(yield* client.withDate({ when: d })).toBe(d.toISOString()); + }), + ); + + it.effect("drops function-valued args (replaced with null)", () => + Effect.gen(function* () { + const handlers = { + take: (arg: { cb: (() => void) | null }) => + Effect.succeed(arg.cb === null ? "null" : typeof arg.cb), + }; + const client = roundTrip(handlers); + expect(yield* client.take({ cb: () => {} })).toBe("null"); + }), + ); + + it.effect("serializes arrays element-wise", () => + Effect.gen(function* () { + const handlers = { + sum: (xs: ReadonlyArray>) => + Effect.succeed(xs.reduce((a, x) => a + Redacted.value(x), 0)), + }; + const client = roundTrip(handlers); + const result = yield* client.sum([ + Redacted.make(1), + Redacted.make(2), + Redacted.make(3), + ]); + expect(result).toBe(6); + }), + ); + + it.effect("converts Output sentinel into a NamedExpr", () => + Effect.gen(function* () { + let received: unknown; + const handlers = { + take: (arg: { val: Output.Output }) => + Effect.sync(() => { + received = arg.val; + return "ok"; + }), + }; + const client = roundTrip(handlers); + const fakeOutput = new Output.NamedExpr( + new Output.EffectExpr(Output.VoidExpr, () => Effect.succeed("x")), + "myBinding", + ); + const result = yield* client.take({ + val: fakeOutput as unknown as Output.Output, + }); + expect(result).toBe("ok"); + expect(Output.isOutput(received)).toBe(true); + expect((received as Output.NamedExpr).kind).toBe("NamedExpr"); + }), + ); + }); + + describe("effect exit serialization", () => { + it.effect("propagates success", () => + Effect.gen(function* () { + const handlers = { + ok: () => Effect.succeed(42), + }; + expect(yield* roundTrip(handlers).ok()).toBe(42); + }), + ); + + it.effect("propagates Fail (typed error)", () => + Effect.gen(function* () { + const handlers = { + boom: (): Effect.Effect => + Effect.fail({ _tag: "Boom" as const, msg: "kaboom" }), + }; + const exit = yield* Effect.exit(roundTrip(handlers).boom()); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.hasFails(exit.cause)).toBe(true); + const found = Cause.findErrorOption(exit.cause); + expect(found._tag).toBe("Some"); + if (found._tag === "Some") { + expect(found.value).toEqual({ _tag: "Boom", msg: "kaboom" }); + } + } + }), + ); + + it.effect("propagates Die (defect)", () => + Effect.gen(function* () { + const handlers = { + boom: () => Effect.die(new Error("defect")), + }; + const exit = yield* Effect.exit(roundTrip(handlers).boom()); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.hasDies(exit.cause)).toBe(true); + } + }), + ); + + it.effect("propagates Interrupt", () => + Effect.gen(function* () { + const handlers = { + boom: () => Effect.interrupt, + }; + const exit = yield* Effect.exit(roundTrip(handlers).boom()); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.hasInterrupts(exit.cause)).toBe(true); + } + }), + ); + }); + + describe("streams", () => { + it.effect("round-trips a stream when streamKeys is honored", () => + Effect.gen(function* () { + const handlers = { + tail: (_n: number) => Stream.fromIterable([1, 2, 3, 4]), + }; + const client = roundTrip(handlers, ["tail"] as const); + const out: Array = []; + yield* Stream.runForEach(client.tail(0), (x) => + Effect.sync(() => { + out.push(x); + }), + ); + expect(out).toEqual([1, 2, 3, 4]); + }), + ); + }); + + describe("nested handlers", () => { + it.effect("recurses into nested handler objects", () => + Effect.gen(function* () { + const handlers = { + group: { + echo: (s: Redacted.Redacted) => + Effect.succeed(Redacted.value(s)), + }, + }; + const client = roundTrip(handlers); + expect(yield* client.group.echo(Redacted.make("nested"))).toBe( + "nested", + ); + }), + ); + }); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServer.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServer.test.ts new file mode 100644 index 00000000000..45b46f5f40f --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServer.test.ts @@ -0,0 +1,115 @@ +import { unwrapRpcHandlers } from "@/Local/RpcSerialization.ts"; +import type { RpcProxyApi } from "@/Local/RpcServer.ts"; +import { PlatformServices } from "@/Util/PlatformServices.ts"; +import { assert, describe, expect, it } from "@effect/vitest"; +import { newWebSocketRpcSession, type RpcStub } from "capnweb"; +import * as Clock from "effect/Clock"; +import * as Effect from "effect/Effect"; +import * as Sink from "effect/Sink"; +import * as Stream from "effect/Stream"; +import * as ChildProcess from "effect/unstable/process/ChildProcess"; +import { openWebSocket, waitForExit } from "./fixtures/process-effect.ts"; +import { runtimes } from "./fixtures/runtimes.ts"; + +const FIXTURE_TS = new URL("./fixtures/rpc-server-entry.ts", import.meta.url) + .pathname; + +const ADDRESS_RE = /(.+?)<\/ALCHEMY_RPC_ADDRESS>/; + +const sampleEnv = () => + JSON.stringify({ + profile: null, + envFile: null, + alchemyContext: { + dotAlchemy: "/tmp/.alchemy", + updateStateStore: false, + dev: true, + adopt: false, + }, + stack: { name: "test", stage: "dev" }, + }); + +for (const runtime of runtimes()) { + describe.skipIf(!runtime.available)( + `Local.RpcServer (${runtime.name})`, + () => { + const [bin, ...args] = runtime.argv(FIXTURE_TS); + const launch = ChildProcess.make(bin, args, { + env: { + ALCHEMY_RPC_SERVER_ENVIRONMENT: sampleEnv(), + }, + extendEnv: true, + // We never write to the child's stdin, so close it. stdout/stderr + // default to "pipe" which is what we want for the buffering forks + // below. + stdin: "ignore", + // SIGTERM first, escalate to SIGKILL after 1s if the child hasn't + // exited. Matches the behavior of the old hand-rolled finalizer. + killSignal: "SIGTERM", + forceKillAfter: "1 second", + }); + + it.live( + "prints the RPC address marker on stdout and accepts /parent + session connections", + () => + Effect.gen(function* () { + const proc = yield* launch; + const url = yield* proc.stdout.pipe( + Stream.decodeText, + Stream.run( + Sink.fold( + () => "", + (acc) => !acc.includes(""), + (acc, chunk) => Effect.succeed(acc + chunk), + ), + ), + Effect.timeout("5 seconds"), + Effect.map((output) => output.match(ADDRESS_RE)?.[1]), + ); + assert(url, `url not found in output: "${url}"`); + expect(url).toMatch(/^ws:\/\//); + + // Open the parent websocket inside the scope so it stays alive + // for the duration of the RPC exchange below; closing it later + // is exactly what triggers the child to exit. + const parent = yield* openWebSocket(new URL("/parent", url)); + + // Drive a real RPC call through a session websocket. capnweb's + // surface is Promise-based, so we wrap exactly at the boundary + // and let everything above and below stay in Effect. + const stub = newWebSocketRpcSession(url) as RpcStub; + const result = yield* Effect.promise(async () => { + const provider = await stub.getProvider("Test.Echo"); + const handlers = unwrapRpcHandlers(provider as any) as { + echo: (msg: string) => Effect.Effect; + }; + return await Effect.runPromise(handlers.echo("hello")); + }); + expect(result).toBe("echo:hello"); + + // Closing the parent ws should cause the child to exit promptly. + yield* Effect.sync(() => parent.close()); + // waitForExit fails if the child is still running after the + // timeout, so reaching this point means the child exited. + yield* waitForExit(proc, "5 seconds"); + }).pipe(Effect.scoped, Effect.provide(PlatformServices)), + { timeout: 30_000 }, + ); + + it.live( + "exits if the parent never connects within ~10s", + () => + Effect.gen(function* () { + const start = yield* Clock.currentTimeMillis; + const proc = yield* launch; + // Never open /parent — the server should self-terminate via the + // connect timeout. + yield* waitForExit(proc, "20 seconds"); + const elapsed = (yield* Clock.currentTimeMillis) - start; + expect(elapsed).toBeLessThan(18_000); + }).pipe(Effect.scoped, Effect.provide(PlatformServices)), + { timeout: 30_000 }, + ); + }, + ); +} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServerEnvironment.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServerEnvironment.test.ts new file mode 100644 index 00000000000..136be22cb75 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServerEnvironment.test.ts @@ -0,0 +1,71 @@ +import { AlchemyContext } from "@/AlchemyContext.ts"; +import { + fromEnv, + layer, + RPC_SERVER_ENVIRONMENT_KEY, + type RpcServerEnvironment, +} from "@/Local/RpcServerEnvironment.ts"; +import { Stack } from "@/Stack.ts"; +import { Stage } from "@/Stage.ts"; +import { PlatformServices } from "@/Util/PlatformServices.ts"; +import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +const sampleEnv: RpcServerEnvironment = { + profile: undefined, + envFile: undefined, + alchemyContext: { + dotAlchemy: "/tmp/.alchemy", + dev: true, + adopt: false, + }, + stack: { + name: "my-stack", + stage: "dev", + }, +}; + +describe("Local.RpcServerEnvironment", () => { + it.effect("layer() provides Stack, Stage, and AlchemyContext", () => + Effect.gen(function* () { + const observed = yield* Effect.gen(function* () { + const stack = yield* Stack; + const stage = yield* Stage; + const ctx = yield* AlchemyContext; + return { stack, stage, ctx }; + }).pipe( + Effect.provide(Layer.provide(layer(sampleEnv), PlatformServices)), + ); + + expect(observed.stack.name).toBe("my-stack"); + expect(observed.stack.stage).toBe("dev"); + expect(observed.stage).toBe("dev"); + expect(observed.ctx.dotAlchemy).toBe("/tmp/.alchemy"); + expect(observed.ctx.dev).toBe(true); + }), + ); + + it.effect("fromEnv() roundtrips a serialized RpcServerEnvironment", () => + Effect.gen(function* () { + // We can't safely mutate `process.env[RPC_SERVER_ENVIRONMENT_KEY]` + // from inside a concurrent test, so we install a private layer in + // front of `fromEnv()` instead and verify it produces the same + // Stack service that `layer()` does. + process.env[RPC_SERVER_ENVIRONMENT_KEY] = JSON.stringify(sampleEnv); + try { + const stack = yield* Stack.pipe( + Effect.provide(Layer.provide(fromEnv(), PlatformServices)), + ); + expect(stack.name).toBe(sampleEnv.stack.name); + expect(stack.stage).toBe(sampleEnv.stack.stage); + } finally { + delete process.env[RPC_SERVER_ENVIRONMENT_KEY]; + } + }), + ); + + it("exports the canonical environment variable key", () => { + expect(RPC_SERVER_ENVIRONMENT_KEY).toBe("ALCHEMY_RPC_SERVER_ENVIRONMENT"); + }); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServerSession.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServerSession.test.ts new file mode 100644 index 00000000000..eac3b49c4c5 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcServerSession.test.ts @@ -0,0 +1,124 @@ +import { + makeServerRpcSession, + type ServerWebSocketLike, +} from "@/Local/RpcServerSession.ts"; +import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; + +describe("Local.RpcServerSession", () => { + it.effect("happy path: paired sessions round-trip a method call", () => + Effect.gen(function* () { + const mainA = { ping: async (x: number) => x + 1 }; + const mainB = { pong: async (x: string) => `${x}!` }; + const { a } = pairSessions(mainA, mainB); + const remote = a.session.getRemoteMain() as unknown as { + pong: (x: string) => Promise; + }; + const result = yield* Effect.promise(() => remote.pong("hi")); + expect(result).toBe("hi!"); + }), + ); + + it.effect("string and Buffer payloads both decode", () => + Effect.gen(function* () { + const received: Array = []; + const ws: ServerWebSocketLike = { + send: () => {}, + close: () => {}, + }; + // Build a session purely to observe `dispatch.message` decoding. + // Use a noop main; we won't call anything on it. + const { dispatch } = makeServerRpcSession(ws, {}); + const origMessage = dispatch.message; + dispatch.message = (data: any) => { + if (typeof data === "string") { + received.push(`str:${data}`); + } else { + received.push(`buf:${data.toString("utf-8")}`); + } + return origMessage(data); + }; + dispatch.message("hello"); + dispatch.message(Buffer.from("world", "utf-8") as Buffer); + expect(received).toEqual(["str:hello", "buf:world"]); + }), + ); + + it.effect("dispatch.message after dispatch.close is dropped silently", () => + Effect.gen(function* () { + const ws: ServerWebSocketLike = { + send: () => {}, + close: () => {}, + }; + const { dispatch } = makeServerRpcSession(ws, {}); + dispatch.close(1000, "bye"); + // Should not throw, should be a no-op. + expect(() => dispatch.message("anything")).not.toThrow(); + }), + ); + + it.effect("dispatch.close rejects a pending getRemoteMain call", () => + Effect.gen(function* () { + // mainB never resolves on the wire because we never deliver anything + // back; the close event should reject the in-flight call. + const closes: Array<{ code?: number; reason?: string }> = []; + const ws: ServerWebSocketLike = { + send: () => { + // intentionally drop outbound traffic + }, + close: (code, reason) => { + closes.push({ code, reason }); + }, + }; + const { session, dispatch } = makeServerRpcSession(ws, {}); + const remote = session.getRemoteMain() as unknown as { + ping: (n: number) => Promise; + }; + const inFlight = remote.ping(1); + dispatch.close(1006, "abnormal"); + // The dropped outbound traffic means the only way `inFlight` can + // resolve is by rejection after `dispatch.close`. Wrap it as an + // Effect that fails on rejection so we can assert with `Exit`. + const exit = yield* Effect.exit(Effect.tryPromise(() => inFlight)); + expect(Exit.isFailure(exit)).toBe(true); + }), + ); +}); + +/** + * Wire two `ServerRpcSession`s together over in-memory fake websockets so we + * can drive `RpcSession.getRemoteMain()` end-to-end without touching the + * platform WS server. + */ +const pairSessions = < + A extends Record, + B extends Record, +>( + mainA: A, + mainB: B, +) => { + const closes: Array<{ side: "a" | "b"; code?: number; reason?: string }> = []; + let a!: ReturnType>; + let b!: ReturnType>; + + const wsA: ServerWebSocketLike = { + send: (msg) => { + queueMicrotask(() => b.dispatch.message(msg)); + }, + close: (code, reason) => { + closes.push({ side: "a", code, reason }); + }, + }; + const wsB: ServerWebSocketLike = { + send: (msg) => { + queueMicrotask(() => a.dispatch.message(msg)); + }, + close: (code, reason) => { + closes.push({ side: "b", code, reason }); + }, + }; + a = makeServerRpcSession(wsA, mainA); + b = makeServerRpcSession(wsB, mainB); + return { a, b, wsA, wsB, closes }; +}; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSpawner.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSpawner.test.ts new file mode 100644 index 00000000000..bedbd800fdb --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSpawner.test.ts @@ -0,0 +1,209 @@ +import { unwrapRpcHandlers } from "@/Local/RpcSerialization.ts"; +import type { RpcProxyApi } from "@/Local/RpcServer.ts"; +import { + layerServer, + RpcSpawner, + type RpcSpawnPayload, +} from "@/Local/RpcSpawner.ts"; +import { PlatformServices } from "@/Util/PlatformServices.ts"; +import { describe, expect, it } from "@effect/vitest"; +import { newWebSocketRpcSession, type RpcStub } from "capnweb"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schedule from "effect/Schedule"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as HttpBody from "effect/unstable/http/HttpBody"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import { + assertPidExited, + canOpenWebSocket, + isAlive, + openWebSocket, + pidListeningOn, +} from "./fixtures/process-effect.ts"; + +const FIXTURE_TS_URL = new URL( + "./fixtures/rpc-server-entry.ts", + import.meta.url, +).toString(); +const CRASH_FIXTURE_TS_URL = new URL( + "./fixtures/rpc-server-crash.ts", + import.meta.url, +).toString(); + +const samplePayload = ( + serverEntryUrl: string, + stackName = "test", +): RpcSpawnPayload => ({ + serverEntryUrl, + alchemyContext: { + dotAlchemy: "/tmp/.alchemy", + dev: true, + adopt: false, + }, + stack: { name: stackName, stage: "dev" }, +}); + +// The spawner inherits the runtime that vitest itself is running under +// (it shells out to `bun` or `node` based on `typeof Bun`). These tests +// only verify behavior for the active runtime; run vitest under both to +// get full coverage. +describe(`Local.RpcSpawner (runtime=${typeof globalThis.Bun !== "undefined" ? "bun" : "node"})`, () => { + /** + * The Spawner layer (and any child processes it spawns) is torn down + * when the surrounding test scope closes, so we provide it at the test + * boundary rather than wrapping a sub-effect — that would tear the + * server down the moment the sub-effect returned. + */ + const services = Layer.provideMerge( + layerServer({ profile: undefined, envFile: undefined }), + Layer.merge(PlatformServices, FetchHttpClient.layer), + ); + + it.live( + "POST returns a ws url whose RPC end-to-end call hits the fixture", + () => + Effect.gen(function* () { + const url = yield* RpcSpawner.useSync((spawner) => spawner.url); + const wsUrl = yield* post(url, samplePayload(FIXTURE_TS_URL)); + expect(wsUrl).toMatch(/^ws:\/\//); + const result = yield* echoWebSocket(wsUrl, "hello"); + expect(result).toBe("echo:hello"); + }).pipe(Effect.provide(services)), + { timeout: 60_000 }, + ); + + it.live( + "caches the child by payload: a second POST returns the same url", + () => + Effect.gen(function* () { + const url = yield* RpcSpawner.useSync((spawner) => spawner.url); + const payload = samplePayload(FIXTURE_TS_URL); + const first = yield* post(url, payload); + const second = yield* post(url, payload); + expect(second).toBe(first); + const pid = yield* pidListeningOn(first); + if (pid !== undefined) { + expect(yield* isAlive(pid)).toBe(true); + } + }).pipe(Effect.provide(services)), + { timeout: 60_000 }, + ); + + it.live( + "distinct payloads spawn distinct children with distinct urls", + () => + Effect.gen(function* () { + const url = yield* RpcSpawner.useSync((spawner) => spawner.url); + const a = yield* post(url, samplePayload(FIXTURE_TS_URL, "stack-a")); + const b = yield* post(url, samplePayload(FIXTURE_TS_URL, "stack-b")); + expect(a).not.toBe(b); + }).pipe(Effect.provide(services)), + { timeout: 60_000 }, + ); + + it.live( + "closing the spawner's scope kills all spawned children", + () => + Effect.gen(function* () { + // Boot the spawner in an inner scope so we can close it while + // the outer test scope is still alive, then assert against the + // pid we recorded. + const pid = yield* Effect.gen(function* () { + const url = yield* RpcSpawner.useSync((spawner) => spawner.url); + const wsUrl = yield* post(url, samplePayload(FIXTURE_TS_URL)); + return yield* pidListeningOn(wsUrl); + }).pipe(Effect.provide(services), Effect.scoped); + + if (pid === undefined) return; + yield* assertPidExited(pid); + }), + { timeout: 60_000 }, + ); + + it.live( + "url returned for a crash-on-boot fixture is not a usable RPC endpoint", + () => + Effect.gen(function* () { + // The crash fixture prints the address marker then exits. The + // spawner's health check is best-effort: depending on race + // timing the POST may return a bogus url, or surface a 500 + // once the retry budget drains. The invariant we *can* + // assert is that callers cannot open a parent websocket to + // the returned url. + const url = yield* RpcSpawner.useSync((spawner) => spawner.url); + yield* Effect.gen(function* () { + const r = yield* postRaw(url, samplePayload(CRASH_FIXTURE_TS_URL)); + if (r.status !== 200) { + return { unusable: true } as const; + } + const usable = yield* canOpenWebSocket(new URL("/parent", r.body)); + return { unusable: !usable } as const; + }).pipe( + Effect.flatMap((r) => + r.unusable + ? Effect.void + : Effect.fail(new Error("endpoint was still usable")), + ), + // Mirrors the original `for (let i = 0; i < 4 && !failed; i++)` + // loop: up to 4 retries spaced 250ms apart. + Effect.retry({ + schedule: Schedule.spaced(Duration.millis(250)), + times: 4, + }), + ); + }).pipe(Effect.provide(services)), + { timeout: 60_000 }, + ); +}); + +interface PostResult { + readonly status: number; + readonly body: string; +} + +const postRaw = ( + url: string, + body: unknown, +): Effect.Effect => + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient; + const req = HttpClientRequest.post(url).pipe( + HttpClientRequest.setBody( + HttpBody.text(JSON.stringify(body), "application/json"), + ), + ); + const res = yield* client.execute(req); + const text = yield* res.text; + return { status: res.status, body: text }; + }).pipe(Effect.orDie); + +const post = ( + url: string, + body: unknown, +): Effect.Effect => + postRaw(url, body).pipe( + Effect.flatMap((r) => + r.status === 200 + ? Effect.succeed(r.body) + : Effect.fail(new Error(`spawn POST failed: ${r.status} ${r.body}`)), + ), + ); + +const echoWebSocket = ( + rpcUrl: string, + msg: string, +): Effect.Effect => + Effect.gen(function* () { + yield* openWebSocket(new URL("/parent", rpcUrl)); + return yield* Effect.promise(async () => { + const stub = newWebSocketRpcSession(rpcUrl) as RpcStub; + const provider = await stub.getProvider("Test.Echo"); + const handlers = unwrapRpcHandlers(provider as any) as { + echo: (m: string) => Effect.Effect; + }; + return await Effect.runPromise(handlers.echo(msg)); + }); + }).pipe(Effect.scoped); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSpawnerCleanup.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSpawnerCleanup.test.ts new file mode 100644 index 00000000000..3e087a447b1 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/RpcSpawnerCleanup.test.ts @@ -0,0 +1,104 @@ +import { PlatformServices } from "@/Util/PlatformServices.ts"; +import { assert, describe, expect, it } from "@effect/vitest"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Sink from "effect/Sink"; +import * as Stream from "effect/Stream"; +import * as ChildProcess from "effect/unstable/process/ChildProcess"; +import { fileURLToPath } from "node:url"; +import { + assertPidExited, + isAlive, + killPid, + pidListeningOn, + waitForExit, +} from "./fixtures/process-effect.ts"; +import { runtimes } from "./fixtures/runtimes.ts"; + +const PARENT_TS = fileURLToPath( + new URL("./fixtures/rpc-spawner-parent.ts", import.meta.url), +); +const CHILD_TS_URL = new URL( + "./fixtures/rpc-server-entry.ts", + import.meta.url, +).toString(); + +for (const runtime of runtimes()) { + describe(`Local.RpcSpawner cleanup (${runtime.name})`, () => { + /** + * Boots the parent fixture and waits until it has reported both its own + * pid and the child's RPC url (from which we resolve the child's pid via + * `lsof`). Retries the stdout parse on a schedule until both fields are + * populated. + */ + const launch = Effect.gen(function* () { + const [bin, ...args] = runtime.argv(PARENT_TS); + const child = yield* ChildProcess.make(bin, [...args, CHILD_TS_URL], { + stdout: "pipe", + forceKillAfter: "1 second", + }); + const output = yield* child.stdout.pipe( + Stream.decodeText, + Stream.run( + Sink.fold( + () => "", + (acc) => + !acc.includes("CHILD_URL=") || !acc.includes("PARENT_PID="), + (acc, chunk) => Effect.succeed(acc + chunk), + ), + ), + Effect.timeout("5 seconds"), + ); + + const childUrl = output.match(/CHILD_URL=(\S+)/)?.[1]; + const parentPid = Number.parseInt( + output.match(/PARENT_PID=(\d+)/)?.[1]!, + 10, + ); + + assert(childUrl, `child url not found in output: ${output}`); + assert( + !Number.isNaN(parentPid), + `parent pid not found in output: ${output}`, + ); + + const childPid = yield* pidListeningOn(childUrl); + + yield* Effect.addFinalizer(() => killPid(childPid, "SIGKILL")); + + return { + child, + parentPid, + childPid, + }; + }); + + it.live( + "child dies after parent receives SIGTERM", + () => + Effect.gen(function* () { + const { child, parentPid, childPid } = yield* launch; + expect(yield* isAlive(childPid)).toBe(true); + yield* killPid(parentPid, "SIGTERM"); + // waitForExit wraps `handle.exitCode`, which resolves once + // the OS reports the parent's exit. + yield* waitForExit(child, Duration.seconds(10)); + yield* assertPidExited(childPid); + }).pipe(Effect.provide(PlatformServices)), + { timeout: 45_000 }, + ); + + it.live( + "child dies after parent receives SIGKILL", + () => + Effect.gen(function* () { + const { child, parentPid, childPid } = yield* launch; + expect(yield* isAlive(childPid)).toBe(true); + yield* killPid(parentPid, "SIGKILL"); + yield* waitForExit(child, Duration.seconds(10)); + yield* assertPidExited(childPid); + }).pipe(Effect.provide(PlatformServices)), + { timeout: 45_000 }, + ); + }); +} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/process-effect.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/process-effect.ts new file mode 100644 index 00000000000..99527669705 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/process-effect.ts @@ -0,0 +1,149 @@ +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Schedule from "effect/Schedule"; +import type * as Scope from "effect/Scope"; +import * as Stream from "effect/Stream"; +import * as ChildProcess from "effect/unstable/process/ChildProcess"; +import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"; + +/** + * Wait for the child to exit (with timeout). Uses `handle.isRunning` + * rather than `handle.exitCode` because the latter raises a + * `PlatformError` for processes killed by signal (no exit code), which + * is a perfectly normal outcome for the SIGKILL test cases. + */ +export const waitForExit = ( + handle: ChildProcessHandle, + timeout: Duration.Input, +): Effect.Effect => + handle.isRunning.pipe( + Effect.orElseSucceed(() => false), + Effect.repeat({ + schedule: Schedule.spaced(Duration.millis(50)), + until: (running) => !running, + }), + Effect.timeout(timeout), + Effect.catchTag("TimeoutError", () => + Effect.fail(new Error("child did not exit in time")), + ), + ); + +export const assertPidExited = (pid: number): Effect.Effect => + isAlive(pid).pipe( + Effect.orElseSucceed(() => false), + Effect.repeat({ + schedule: Schedule.spaced(Duration.millis(50)), + until: (alive) => !alive, + }), + Effect.timeout("5 seconds"), + Effect.catchTag("TimeoutError", () => + Effect.fail(new Error("child did not exit in time")), + ), + ); + +/** + * `process.kill(pid, 0)` is a sync syscall that probes a pid we don't + * own a handle to (e.g. a grandchild spawned by the parent fixture). + * Wrapped in `Effect.sync` so it participates in the runtime. + */ +export const isAlive = (pid: number): Effect.Effect => + Effect.sync(() => { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } + }); + +/** + * Resolves the pid currently LISTENing on the port of `wsUrl`. Uses an + * `lsof` invocation; we don't own a handle to whatever process is + * listening so there's no ChildProcessHandle equivalent. + */ +export const pidListeningOn = (wsUrl: string) => + ChildProcess.make( + "lsof", + ["-iTCP:" + new URL(wsUrl).port, "-sTCP:LISTEN", "-t"], + { + stdout: "pipe", + }, + ).pipe( + Effect.flatMap((handle) => + handle.stdout.pipe(Stream.decodeText, Stream.mkString), + ), + Effect.map((stdout) => Number.parseInt(stdout.trim().split("\n")[0]!, 10)), + ); + +/** Send a signal to a pid we don't own a handle to. */ +export const killPid = ( + pid: number, + signal: NodeJS.Signals, +): Effect.Effect => + Effect.sync(() => { + try { + process.kill(pid, signal); + } catch {} + }); + +/** + * Open a WebSocket inside a scope so it's reliably closed at scope + * end. Resolves once `open` fires or fails on error / close. + */ +export const openWebSocket = ( + url: string | URL, +): Effect.Effect => + Effect.acquireRelease( + Effect.callback((resume) => { + const ws = new WebSocket(url); + const cleanup = () => { + ws.removeEventListener("open", onOpen); + ws.removeEventListener("error", onError); + }; + const onOpen = () => { + cleanup(); + resume(Effect.succeed(ws)); + }; + const onError = () => { + cleanup(); + try { + ws.close(); + } catch {} + resume(Effect.fail(new Error(`websocket connect failed: ${url}`))); + }; + ws.addEventListener("open", onOpen, { once: true }); + ws.addEventListener("error", onError, { once: true }); + }), + (ws) => + Effect.sync(() => { + try { + ws.close(); + } catch {} + }), + ); + +/** Probe whether a websocket can be opened. Never fails. */ +export const canOpenWebSocket = ( + url: string | URL, + timeout: Duration.Input = Duration.millis(1_500), +): Effect.Effect => + Effect.callback((resume) => { + const ws = new WebSocket(url); + let settled = false; + const settle = (v: boolean) => { + if (settled) return; + settled = true; + try { + ws.close(); + } catch {} + resume(Effect.succeed(v)); + }; + ws.addEventListener("open", () => settle(true), { once: true }); + ws.addEventListener("error", () => settle(false), { once: true }); + ws.addEventListener("close", () => settle(false), { once: true }); + }).pipe( + Effect.timeoutOrElse({ + duration: timeout, + orElse: () => Effect.succeed(false), + }), + ); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-server-crash.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-server-crash.ts new file mode 100644 index 00000000000..ddece86d32f --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-server-crash.ts @@ -0,0 +1,5 @@ +// Fixture that prints the marker and then immediately exits, simulating an +// RPC server that boots but crashes before the parent can use it. Used to +// exercise the spawner's retry-budget logic. +console.log("ws://127.0.0.1:1/"); +process.exit(1); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-server-entry.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-server-entry.ts new file mode 100644 index 00000000000..e4e8094fe11 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-server-entry.ts @@ -0,0 +1,28 @@ +// Relative import (not `@/` alias) so this file runs under both Bun and Node +// without a paths-aware loader. This fixture is excluded from the test +// project's typecheck (see tsconfig.test.json) because the relative path +// crosses composite-project boundaries. +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { launch } from "../../../src/Local/RpcServer.ts"; + +/** + * Minimal test fixture for `RpcServer.launch`. Registers a single service + * keyed by `"Test.Echo"` which is what the parent looks up via + * `getProvider("Test.Echo")`. + */ +export class TestEcho extends Context.Service< + TestEcho, + { + echo: (msg: string) => Effect.Effect; + boom: () => Effect.Effect; + } +>()("Test.Echo") {} + +const TestEchoLive = Layer.succeed(TestEcho, { + echo: (msg) => Effect.succeed(`echo:${msg}`), + boom: () => Effect.fail({ _tag: "Boom" as const, msg: "kaboom" }), +}); + +launch(TestEchoLive); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-spawner-parent.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-spawner-parent.ts new file mode 100644 index 00000000000..ec02d58bac0 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/rpc-spawner-parent.ts @@ -0,0 +1,65 @@ +// Test fixture: boots an RpcSpawner, POSTs once with the child entry given by +// the first CLI arg, and prints `PARENT_PID=` and `CHILD_PID=` lines to +// stdout so the test harness can observe them. Then idles until killed. +// Relative imports (not `@/` alias) so this file runs under both Bun and +// Node without a paths-aware loader. This fixture is excluded from the test +// project's typecheck (see tsconfig.test.json) because the relative path +// crosses composite-project boundaries. +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as HttpBody from "effect/unstable/http/HttpBody"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import { layerServer, RpcSpawner } from "../../../src/Local/RpcSpawner.ts"; +import { PlatformServices } from "../../../src/Util/PlatformServices.ts"; + +const childEntry = process.argv[2]; +if (!childEntry) { + console.error("usage: rpc-spawner-parent.ts "); + process.exit(2); +} + +const program = Effect.gen(function* () { + const sp = yield* RpcSpawner; + const http = yield* HttpClient.HttpClient; + const res = yield* http + .post(sp.url, { + body: yield* HttpBody.json({ + serverEntryUrl: childEntry, + alchemyContext: { + dotAlchemy: "/tmp/.alchemy", + updateStateStore: false, + dev: true, + adopt: false, + }, + stack: { name: "test", stage: "dev" }, + }), + }) + .pipe(Effect.flatMap((res) => res.text)); + + // The child's pid is whatever owns the listening port returned in res. + // We surface it for the test harness via stdout. + console.log(`PARENT_PID=${process.pid}\n`); + console.log(`CHILD_URL=${res}\n`); + + const stop = yield* Deferred.make(); + yield* Deferred.await(stop); +}); + +program + .pipe( + Effect.provide([ + Layer.provide( + layerServer({ profile: undefined, envFile: undefined }), + PlatformServices, + ), + FetchHttpClient.layer, + ]), + Effect.scoped, + Effect.runPromise, + ) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/runtimes.ts b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/runtimes.ts new file mode 100644 index 00000000000..f289054e1b1 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Local/fixtures/runtimes.ts @@ -0,0 +1,34 @@ +import { spawnSync } from "node:child_process"; + +export interface Runtime { + readonly name: "bun" | "node"; + readonly argv: (entry: string) => Array; + readonly available: boolean; +} + +const hasBin = (bin: string): boolean => { + try { + const r = spawnSync("which", [bin], { encoding: "utf-8" }); + return r.status === 0 && Boolean(r.stdout?.trim()); + } catch { + return false; + } +}; + +export const runtimes = (): Array => [ + { + name: "bun", + argv: (entry) => ["bun", "run", entry], + available: hasBin("bun"), + }, + { + name: "node", + argv: (entry) => [ + "node", + "--experimental-transform-types", + "--no-warnings=ExperimentalWarning", + entry, + ], + available: hasBin("node"), + }, +]; diff --git a/.repos/alchemy-effect/packages/alchemy/test/Neon/Project.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Neon/Project.test.ts new file mode 100644 index 00000000000..12464dc60b6 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Neon/Project.test.ts @@ -0,0 +1,122 @@ +import * as Neon from "@/Neon"; +import * as Test from "@/Test/Vitest"; +import { getProject } from "@distilled.cloud/neon"; +import { expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import { MinimumLogLevel } from "effect/References"; + +const { test } = Test.make({ providers: Neon.providers() }); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +test.provider("create and delete project with default props", (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const project = yield* stack.deploy( + Effect.gen(function* () { + return yield* Neon.Project("DefaultProject"); + }), + ); + + expect(project.projectId).toBeDefined(); + expect(project.projectName).toBeDefined(); + expect(project.defaultBranchId).toBeDefined(); + expect(project.connectionUri).toContain("postgres"); + + const fetched = yield* getProject({ project_id: project.projectId }); + expect(fetched.project.id).toEqual(project.projectId); + + yield* stack.destroy(); + }).pipe(logLevel), +); + +test.provider("enable logical replication on update", (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const initial = yield* stack.deploy( + Effect.gen(function* () { + return yield* Neon.Project("LogicalReplicationProject", { + name: "alchemy-test-logical-replication", + region: "aws-us-east-1", + }); + }), + ); + expect(initial.enableLogicalReplication).toEqual(false); + + const enabled = yield* stack.deploy( + Effect.gen(function* () { + return yield* Neon.Project("LogicalReplicationProject", { + name: "alchemy-test-logical-replication", + region: "aws-us-east-1", + enableLogicalReplication: true, + }); + }), + ); + expect(enabled.projectId).toEqual(initial.projectId); + expect(enabled.enableLogicalReplication).toEqual(true); + + const fetched = yield* getProject({ project_id: enabled.projectId }); + expect(fetched.project.settings).toMatchObject({ + enable_logical_replication: true, + }); + + yield* stack.destroy(); + }).pipe(logLevel), +); + +test.provider( + "create project, apply migrations and seed data, then create a branch", + (stack) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const migrationsDir = yield* fs.makeTempDirectory({ + prefix: "alchemy-neon-migrations-", + }); + yield* fs.writeFileString( + path.join(migrationsDir, "0001_users.sql"), + "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL);", + ); + const seedDir = yield* fs.makeTempDirectory({ + prefix: "alchemy-neon-seed-", + }); + const seedPath = path.join(seedDir, "seed.sql"); + yield* fs.writeFileString( + seedPath, + "INSERT INTO users (name) VALUES ('alice'), ('bob');", + ); + + yield* stack.destroy(); + + const { project, branch } = yield* stack.deploy( + Effect.gen(function* () { + const project = yield* Neon.Project("MigrationProject", { + migrationsDir, + importFiles: [seedPath], + }); + const branch = yield* Neon.Branch("FeatureBranch", { + project, + }); + return { project, branch }; + }), + ); + + expect(project.migrationsTable).toEqual("neon_migrations"); + expect(Object.keys(project.migrationsHashes).sort()).toEqual([ + "0001_users.sql", + ]); + expect(project.importHashes[seedPath]).toBeDefined(); + + expect(branch.projectId).toEqual(project.projectId); + expect(branch.parentBranchId).toEqual(project.defaultBranchId); + + yield* stack.destroy(); + }).pipe(logLevel), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Output.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Output.test.ts new file mode 100644 index 00000000000..7b87c06cd35 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Output.test.ts @@ -0,0 +1,911 @@ +import * as Output from "@/Output"; +import { ref as makeRef } from "@/Ref"; +import type { ResourceLike } from "@/Resource"; +import { Stack } from "@/Stack"; +import { Stage } from "@/Stage"; +import { inMemoryState } from "@/State/InMemoryState"; +import type { ResourceState } from "@/State/ResourceState"; +import { describe, expect, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; + +const provideState = (effect: Effect.Effect) => + effect.pipe(Effect.provide(inMemoryState())); + +const fakeResource = ( + type: T, + fqn: string, + logicalId: string = fqn, +): ResourceLike => + ({ + Type: type, + FQN: fqn, + LogicalId: logicalId, + Namespace: undefined, + }) as any; + +describe("Output.evaluate", () => { + describe("primitives and plain values", () => { + it.effect("returns primitive values as-is", () => + provideState( + Effect.gen(function* () { + expect(yield* Output.evaluate(42, {})).toBe(42); + expect(yield* Output.evaluate("hello", {})).toBe("hello"); + expect(yield* Output.evaluate(true, {})).toBe(true); + expect(yield* Output.evaluate(null, {})).toBe(null); + expect(yield* Output.evaluate(undefined, {})).toBe(undefined); + }), + ), + ); + + it.effect("recursively evaluates plain objects", () => + provideState( + Effect.gen(function* () { + const result = yield* Output.evaluate({ a: 1, b: { c: "x" } }, {}); + expect(result).toEqual({ a: 1, b: { c: "x" } }); + }), + ), + ); + + it.effect("recursively evaluates arrays", () => + provideState( + Effect.gen(function* () { + const result = yield* Output.evaluate([1, "two", { k: 3 }], {}); + expect(result).toEqual([1, "two", { k: 3 }]); + }), + ), + ); + }); + + describe("Redacted", () => { + it.effect("preserves Redacted values at the top level", () => + provideState( + Effect.gen(function* () { + const secret = Redacted.make("hunter2"); + const result = yield* Output.evaluate(secret, {}); + expect(Redacted.isRedacted(result)).toBe(true); + expect(Redacted.value(result as Redacted.Redacted)).toBe( + "hunter2", + ); + }), + ), + ); + + it.effect("preserves Redacted values nested inside an object", () => + provideState( + Effect.gen(function* () { + const secret = Redacted.make("hunter2"); + const result = yield* Output.evaluate( + { value: secret, name: "x" }, + {}, + ); + expect(result.name).toBe("x"); + expect(Redacted.isRedacted(result.value)).toBe(true); + expect(Redacted.value(result.value)).toBe("hunter2"); + }), + ), + ); + + it.effect("preserves Redacted values nested inside an array", () => + provideState( + Effect.gen(function* () { + const secret = Redacted.make("hunter2"); + const [result] = yield* Output.evaluate([secret], {}); + expect(Redacted.isRedacted(result)).toBe(true); + expect(Redacted.value(result)).toBe("hunter2"); + }), + ), + ); + }); + + describe("LiteralExpr", () => { + it.effect("evaluates Output.literal(value)", () => + provideState( + Effect.gen(function* () { + const expr = Output.literal("foo"); + expect(yield* Output.evaluate(expr, {})).toBe("foo"); + }), + ), + ); + + it.effect("evaluates a literal nested within an object", () => + provideState( + Effect.gen(function* () { + const result = yield* Output.evaluate( + { greeting: Output.literal("hi") }, + {}, + ); + expect(result).toEqual({ greeting: "hi" }); + }), + ), + ); + }); + + describe("ResourceExpr", () => { + it.effect("resolves to the upstream value keyed by FQN", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "MyBucket"); + const expr = Output.of(src); + const result = yield* Output.evaluate(expr, { + MyBucket: { name: "my-bucket" }, + }); + expect(result).toEqual({ name: "my-bucket" }); + }), + ), + ); + + it.effect("fails with MissingSourceError when upstream is absent", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "Missing"); + const expr = Output.of(src); + const exit = yield* Effect.exit(Output.evaluate(expr, {})); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const failure = exit.cause.toJSON() as any; + expect(JSON.stringify(failure)).toContain("MissingSourceError"); + } + }), + ), + ); + + it.effect("evaluates a raw resource (isResource branch)", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "RawBucket"); + const result = yield* Output.evaluate(src as any, { + RawBucket: { ok: true }, + }); + expect(result).toEqual({ ok: true }); + }), + ), + ); + + it.effect("raw resource with missing upstream fails", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "Gone"); + const exit = yield* Effect.exit(Output.evaluate(src as any, {})); + expect(Exit.isFailure(exit)).toBe(true); + }), + ), + ); + }); + + describe("PropExpr", () => { + it.effect("accesses a property on a resource expression", () => + provideState( + Effect.gen(function* () { + const src = fakeResource<"Test.Bucket", { name: string }>( + "Test.Bucket", + "B", + ); + const expr = Output.of(src) as any; + const result = yield* Output.evaluate(expr.name, { + B: { name: "the-name" }, + }); + expect(result).toBe("the-name"); + }), + ), + ); + + it.effect("returns undefined when accessing missing property", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "B2"); + const expr = Output.of(src) as any; + const result = yield* Output.evaluate(expr.missing, { + B2: { other: 1 }, + }); + expect(result).toBeUndefined(); + }), + ), + ); + + it.effect("supports nested property access", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "B3"); + const expr = Output.of(src) as any; + const result = yield* Output.evaluate(expr.nested.deep, { + B3: { nested: { deep: "value" } }, + }); + expect(result).toBe("value"); + }), + ), + ); + }); + + describe("ApplyExpr (map)", () => { + it.effect("applies a synchronous function over a literal", () => + provideState( + Effect.gen(function* () { + const expr = Output.map(Output.literal(2), (n) => n * 3); + expect(yield* Output.evaluate(expr, {})).toBe(6); + }), + ), + ); + + it.effect("composes multiple maps", () => + provideState( + Effect.gen(function* () { + const expr = Output.literal(2).pipe( + Output.map((n: number) => n + 1), + Output.map((n: number) => n * 10), + ); + expect(yield* Output.evaluate(expr, {})).toBe(30); + }), + ), + ); + + it.effect("maps over a resource attribute", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "B4"); + const expr = (Output.of(src) as any).name.pipe( + Output.map((s: string) => s.toUpperCase()), + ); + const result = yield* Output.evaluate(expr, { + B4: { name: "abc" }, + }); + expect(result).toBe("ABC"); + }), + ), + ); + }); + + describe("EffectExpr (mapEffect)", () => { + it.effect("evaluates an effectful transformation", () => + provideState( + Effect.gen(function* () { + const expr = Output.literal(5).pipe( + Output.mapEffect((n: number) => Effect.succeed(n * 2)), + ); + expect(yield* Output.evaluate(expr, {})).toBe(10); + }), + ), + ); + + it.effect("chains multiple effectful transformations", () => + provideState( + Effect.gen(function* () { + const expr = Output.literal("a").pipe( + Output.mapEffect((s: string) => Effect.succeed(s + "b")), + Output.mapEffect((s) => Effect.succeed(s + "c")), + ); + expect(yield* Output.evaluate(expr, {})).toBe("abc"); + }), + ), + ); + }); + + describe("FlatMapExpr (flatMap)", () => { + it.effect("flattens an Output returned from the function", () => + provideState( + Effect.gen(function* () { + const expr = Output.literal(5).pipe( + Output.flatMap((n: number) => Output.literal(n * 2)), + ); + expect(yield* Output.evaluate(expr, {})).toBe(10); + }), + ), + ); + + it.effect("supports the data-first form", () => + provideState( + Effect.gen(function* () { + const expr = Output.flatMap(Output.literal("a"), (s: string) => + Output.literal(s + "b"), + ); + expect(yield* Output.evaluate(expr, {})).toBe("ab"); + }), + ), + ); + + it.effect("chains multiple flatMaps", () => + provideState( + Effect.gen(function* () { + const expr = Output.literal(1).pipe( + Output.flatMap((n: number) => Output.literal(n + 1)), + Output.flatMap((n: number) => Output.literal(n * 10)), + ); + expect(yield* Output.evaluate(expr, {})).toBe(20); + }), + ), + ); + + it.effect("flatMaps into another resource's output", () => + provideState( + Effect.gen(function* () { + const a = fakeResource("Test.A", "FA"); + const b = fakeResource("Test.B", "FB"); + const expr = (Output.of(a) as any).name.pipe( + Output.flatMap(() => (Output.of(b) as any).name), + ); + const result = yield* Output.evaluate(expr, { + FA: { name: "a-name" }, + FB: { name: "b-name" }, + }); + expect(result).toBe("b-name"); + }), + ), + ); + + it.effect("can flatMap into a non-Output literal value", () => + provideState( + Effect.gen(function* () { + const expr = Output.literal(3).pipe( + Output.flatMap((n: number) => Output.asOutput(n + 1)), + ); + expect(yield* Output.evaluate(expr, {})).toBe(4); + }), + ), + ); + + it("tracks only the source expression as upstream", () => { + const a = fakeResource("Test.A", "UA"); + const expr = (Output.of(a) as any).name.pipe( + Output.flatMap((s: string) => Output.literal(s)), + ); + expect(Object.keys(Output.upstream(expr))).toEqual(["UA"]); + }); + }); + + describe("method-style combinators on a proxied Output", () => { + // Resource outputs are Proxies, so `.map(fn)` / `.mapEffect(fn)` / + // `.flatMap(fn)` / `.apply(fn)` / `.effect(fn)` must be callable as + // methods (not just via `Output.map(output, fn)` / `.pipe(...)`). + const resourceOutput = () => { + const src = fakeResource<"Test.Bucket", { name: string }>( + "Test.Bucket", + "M", + ); + return (Output.of(src) as any).name as Output.Output; + }; + + it.effect(".map(fn) builds an ApplyExpr", () => + provideState( + Effect.gen(function* () { + const expr = (resourceOutput() as any).map((s: string) => + s.toUpperCase(), + ); + expect(Output.isApplyExpr(expr)).toBe(true); + expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe( + "ABC", + ); + }), + ), + ); + + it.effect(".apply(fn) builds an ApplyExpr", () => + provideState( + Effect.gen(function* () { + const expr = (resourceOutput() as any).apply((s: string) => + s.toUpperCase(), + ); + expect(Output.isApplyExpr(expr)).toBe(true); + expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe( + "ABC", + ); + }), + ), + ); + + it.effect(".mapEffect(fn) builds an EffectExpr", () => + provideState( + Effect.gen(function* () { + const expr = (resourceOutput() as any).mapEffect((s: string) => + Effect.succeed(s + "!"), + ); + expect(Output.isEffectExpr(expr)).toBe(true); + expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe( + "abc!", + ); + }), + ), + ); + + it.effect(".effect(fn) builds an EffectExpr", () => + provideState( + Effect.gen(function* () { + const expr = (resourceOutput() as any).effect((s: string) => + Effect.succeed(s + "!"), + ); + expect(Output.isEffectExpr(expr)).toBe(true); + expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe( + "abc!", + ); + }), + ), + ); + + it.effect(".flatMap(fn) builds a FlatMapExpr", () => + provideState( + Effect.gen(function* () { + const expr = (resourceOutput() as any).flatMap((s: string) => + Output.literal(s + "?"), + ); + expect(Output.isFlatMapExpr(expr)).toBe(true); + expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe( + "abc?", + ); + }), + ), + ); + }); + + describe("AllExpr", () => { + it.effect("evaluates all wrapped outputs in parallel", () => + provideState( + Effect.gen(function* () { + const expr = Output.all( + Output.literal(1), + Output.literal("two"), + Output.literal(true), + ); + const result = yield* Output.evaluate(expr, {}); + expect(result).toEqual([1, "two", true]); + }), + ), + ); + + it.effect("evaluates all with resource expressions", () => + provideState( + Effect.gen(function* () { + const a = fakeResource("Test.A", "A"); + const b = fakeResource("Test.B", "B"); + const expr = Output.all(Output.of(a), Output.of(b)); + const result = yield* Output.evaluate(expr, { + A: { x: 1 }, + B: { y: 2 }, + }); + expect(result).toEqual([{ x: 1 }, { y: 2 }]); + }), + ), + ); + }); + + describe("RefExpr", () => { + const provideStackStage = ( + effect: Effect.Effect, + stack = "myStack", + stage = "myStage", + ) => + effect.pipe( + Effect.provide( + Layer.mergeAll( + Layer.succeed(Stack, { name: stack } as any), + Layer.succeed(Stage, stage), + ), + ), + ); + + it.effect("resolves a Ref against in-memory state", () => + Effect.gen(function* () { + const initial = { + myStack: { + myStage: { + myResource: { + fqn: "myResource", + attr: { hello: "world" }, + } as unknown as ResourceState, + }, + }, + }; + const r = makeRef("myResource"); + const expr = Output.of(r); + const result = yield* provideStackStage( + Output.evaluate(expr, {}).pipe( + Effect.provide(inMemoryState(initial)), + ), + ); + expect(result).toEqual({ hello: "world" }); + }), + ); + + it.effect("uses explicit stack/stage when provided on the ref", () => + Effect.gen(function* () { + const initial = { + otherStack: { + otherStage: { + someResource: { + fqn: "someResource", + attr: { v: 1 }, + } as unknown as ResourceState, + }, + }, + }; + const r = makeRef("someResource", { + stack: "otherStack", + stage: "otherStage", + }); + const expr = Output.of(r); + // No Stack/Stage layers needed since the ref carries them. + const result = yield* Output.evaluate(expr, {}).pipe( + Effect.provide(inMemoryState(initial)), + ); + expect(result).toEqual({ v: 1 }); + }), + ); + + it.effect( + "fails with InvalidReferenceError when ref target is missing", + () => + Effect.gen(function* () { + const r = makeRef("ghost", { + stack: "s", + stage: "t", + }); + const expr = Output.of(r); + const exit = yield* Effect.exit( + Output.evaluate(expr, {}).pipe(Effect.provide(inMemoryState())), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(JSON.stringify(exit.cause.toJSON())).toContain( + "InvalidReferenceError", + ); + } + }), + ); + + it.effect( + "PropExpr on a Ref reads the attribute from persisted state", + () => + Effect.gen(function* () { + const initial = { + myStack: { + myStage: { + shared: { + fqn: "shared", + attr: { url: "https://example.com", name: "shared" }, + } as unknown as ResourceState, + }, + }, + }; + const r = makeRef("shared"); + const expr = (Output.of(r) as any).url as Output.Output; + const result = yield* provideStackStage( + Output.evaluate(expr, {}).pipe( + Effect.provide(inMemoryState(initial)), + ), + ); + expect(result).toBe("https://example.com"); + }), + ); + }); + + describe("StackRefExpr", () => { + const provideStage = ( + effect: Effect.Effect, + stage = "myStage", + ) => effect.pipe(Effect.provide(Layer.succeed(Stage, stage))); + + it.effect("Output.stackRef resolves to the persisted stack output", () => + Effect.gen(function* () { + const expr = yield* Output.stackRef<{ url: string }>("Backend"); + const result = yield* provideStage( + Output.evaluate(expr, {}).pipe( + Effect.provide( + inMemoryState( + {}, + { Backend: { myStage: { url: "https://api.example.com" } } }, + ), + ), + ), + ); + expect(result).toEqual({ url: "https://api.example.com" }); + }), + ); + + it.effect("explicit stage overrides the ambient Stage", () => + Effect.gen(function* () { + const expr = yield* Output.stackRef<{ url: string }>("Backend", { + stage: "prod", + }); + // No Stage layer is provided — the ref carries it explicitly. + const result = yield* Output.evaluate(expr, {}).pipe( + Effect.provide( + inMemoryState( + {}, + { Backend: { prod: { url: "https://prod.example.com" } } }, + ), + ), + ); + expect(result).toEqual({ url: "https://prod.example.com" }); + }), + ); + + it.effect("PropExpr off a stackRef reads a single attribute", () => + Effect.gen(function* () { + const backend = yield* Output.stackRef<{ url: string }>("Backend"); + const expr = (backend as any).url as Output.Output; + const result = yield* provideStage( + Output.evaluate(expr, {}).pipe( + Effect.provide( + inMemoryState( + {}, + { Backend: { myStage: { url: "https://api.example.com" } } }, + ), + ), + ), + ); + expect(result).toBe("https://api.example.com"); + }), + ); + + it.effect( + "fails with InvalidReferenceError when the target stack/stage has no persisted output", + () => + Effect.gen(function* () { + const expr = yield* Output.stackRef<{ url: string }>("Backend", { + stage: "ghost", + }); + const exit = yield* Effect.exit( + Output.evaluate(expr, {}).pipe(Effect.provide(inMemoryState())), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const err = Cause.squash( + exit.cause, + ) as Output.InvalidReferenceError; + expect(err._tag).toBe("InvalidReferenceError"); + expect(err.stack).toBe("Backend"); + expect(err.stage).toBe("ghost"); + } + }), + ); + }); + + describe("composition", () => { + it.effect("evaluates outputs nested inside arrays and objects", () => + provideState( + Effect.gen(function* () { + const a = fakeResource("Test.A", "RA"); + const b = fakeResource("Test.B", "RB"); + const value = { + list: [Output.of(a), Output.literal("lit")], + nested: { + prop: (Output.of(b) as any).name.pipe( + Output.map((s: string) => `name=${s}`), + ), + }, + scalar: 42, + }; + const result = yield* Output.evaluate(value, { + RA: { foo: "f" }, + RB: { name: "bee" }, + }); + expect(result).toEqual({ + list: [{ foo: "f" }, "lit"], + nested: { prop: "name=bee" }, + scalar: 42, + }); + }), + ), + ); + }); +}); + +describe("Output.interpolate", () => { + it.effect("interpolates literal values into a template", () => + provideState( + Effect.gen(function* () { + const expr = Output.interpolate`hello ${Output.literal("world")}!`; + expect(yield* Output.evaluate(expr, {})).toBe("hello world!"); + }), + ), + ); + + it.effect("interpolates resource attributes", () => + provideState( + Effect.gen(function* () { + const src = fakeResource("Test.Bucket", "Buck"); + // @ts-expect-error + const name = Output.of(src).name; + const expr = Output.interpolate`s3://${name}/key`; + const result = yield* Output.evaluate(expr, { + Buck: { name: "my-bucket" }, + }); + expect(result).toBe("s3://my-bucket/key"); + }), + ), + ); + + it.effect("renders nullish args as empty strings", () => + provideState( + Effect.gen(function* () { + const expr = Output.interpolate`a${Output.literal(null)}b${Output.literal( + undefined, + )}c`; + expect(yield* Output.evaluate(expr, {})).toBe("abc"); + }), + ), + ); +}); + +describe("Output coercion guard", () => { + // These guard against the long-standing footgun where coercing an + // unresolved Output silently produced a placeholder — the inspect + // string for string-hints, or NaN for number-hints. Both let bogus + // values flow into resource props and ultimately into the cloud. + + it("throws on template-literal interpolation of a raw Output", () => { + const src = fakeResource("Test.Bucket", "Buck"); + // @ts-expect-error — synthetic prop access + const name = Output.of(src).name; + expect(() => `${name}`).toThrow(/Output\.interpolate/); + }); + + it("throws on string concatenation with a raw Output", () => { + const src = fakeResource("Test.Bucket", "Buck"); + // @ts-expect-error — synthetic prop access + const name = Output.of(src).name; + expect(() => "s3://" + name).toThrow(/Output\.interpolate/); + }); + + it("throws on number coercion of a raw Output", () => { + const src = fakeResource("Test.Bucket", "Buck"); + // @ts-expect-error — synthetic prop access + const name = Output.of(src).name; + expect(() => +name).toThrow(/Output\.(interpolate|map)/); + }); + + it("throws on arithmetic with a raw Output", () => { + const src = fakeResource("Test.Bucket", "Buck"); + // @ts-expect-error — synthetic prop access + const name = Output.of(src).name; + expect(() => (name as unknown as number) * 2).toThrow( + /Output\.(interpolate|map)/, + ); + }); +}); + +describe("Output.isOutput / isExpr", () => { + it("identifies Output expressions", () => { + expect(Output.isOutput(Output.literal(1))).toBe(true); + expect(Output.isOutput(Output.all(Output.literal(1)))).toBe(true); + expect(Output.isExpr(Output.literal(1))).toBe(true); + }); + + it("rejects non-Output values", () => { + expect(Output.isOutput(1)).toBeFalsy(); + expect(Output.isOutput("x")).toBeFalsy(); + expect(Output.isOutput(null)).toBeFalsy(); + expect(Output.isOutput(undefined)).toBeFalsy(); + expect(Output.isOutput({})).toBeFalsy(); + expect(Output.isOutput([])).toBeFalsy(); + }); +}); + +describe("Output.asOutput", () => { + it.effect("wraps a plain value as a literal Output", () => + provideState( + Effect.gen(function* () { + const o = Output.asOutput("foo"); + expect(Output.isOutput(o)).toBe(true); + expect(yield* Output.evaluate(o, {})).toBe("foo"); + }), + ), + ); + + it.effect("wraps an Effect as an EffectExpr", () => + provideState( + Effect.gen(function* () { + const o = Output.asOutput(Effect.succeed(123)); + expect(Output.isOutput(o)).toBe(true); + expect(yield* Output.evaluate(o, {})).toBe(123); + }), + ), + ); + + it("returns the same Output if already an Output", () => { + const o = Output.literal("x"); + expect(Output.asOutput(o)).toBe(o); + }); +}); + +describe("Output.upstream / hasOutputs / resolveUpstream", () => { + it("returns upstream resources from a ResourceExpr", () => { + const src = fakeResource("Test.A", "FQN-A"); + const expr = Output.of(src); + const up = Output.upstream(expr); + expect(Object.keys(up)).toEqual(["FQN-A"]); + }); + + it("returns upstream resources from a PropExpr", () => { + const src = fakeResource("Test.A", "FQN-A"); + const expr = (Output.of(src) as any).foo; + expect(Object.keys(Output.upstream(expr))).toEqual(["FQN-A"]); + }); + + it("merges upstream resources from AllExpr", () => { + const a = fakeResource("Test.A", "A"); + const b = fakeResource("Test.B", "B"); + const expr = Output.all(Output.of(a), Output.of(b)); + expect(Object.keys(Output.upstream(expr)).sort()).toEqual(["A", "B"]); + }); + + it("returns empty upstream for literals", () => { + expect(Output.upstream(Output.literal(1))).toEqual({}); + }); + + it("treats a raw Resource passed directly as an upstream dependency", () => { + const src = fakeResource("Test.A", "RawA"); + expect(Object.keys(Output.upstream(src as any))).toEqual(["RawA"]); + }); + + it("upstreamAny detects a raw Resource passed directly as a prop value", () => { + const a = fakeResource("Test.A", "FQN-A"); + const b = fakeResource("Test.B", "FQN-B"); + const props = { image: a, network: b }; + expect(Object.keys(Output.upstreamAny(props)).sort()).toEqual([ + "FQN-A", + "FQN-B", + ]); + }); + + it("upstreamAny detects raw Resources nested in arrays/objects", () => { + const a = fakeResource("Test.A", "FQN-A"); + const b = fakeResource("Test.B", "FQN-B"); + const props = { + volumes: [{ source: a }], + env: { ref: b }, + }; + expect(Object.keys(Output.upstreamAny(props)).sort()).toEqual([ + "FQN-A", + "FQN-B", + ]); + }); + + it("upstreamAny detects raw Resource at the top level", () => { + const a = fakeResource("Test.A", "Top"); + expect(Object.keys(Output.upstreamAny(a))).toEqual(["Top"]); + }); + + it("resolveUpstream picks up raw Resources alongside Output expressions", () => { + const a = fakeResource("Test.A", "A1"); + const b = fakeResource("Test.B", "B1"); + const result = Output.resolveUpstream({ + raw: a, + via: Output.of(b), + }); + expect(Object.keys(result).sort()).toEqual(["A1", "B1"]); + }); + + it("hasOutputs is true when an object contains an Output referencing a resource", () => { + const src = fakeResource("Test.A", "X"); + expect(Output.hasOutputs({ k: Output.of(src) })).toBe(true); + }); + + it("hasOutputs is false for plain values", () => { + expect(Output.hasOutputs({ k: 1, b: "x" })).toBe(false); + expect(Output.hasOutputs([1, 2, 3])).toBe(false); + }); + + it("resolveUpstream walks arrays and objects to gather resources", () => { + const a = fakeResource("Test.A", "RA"); + const b = fakeResource("Test.B", "RB"); + const result = Output.resolveUpstream({ + arr: [Output.of(a)], + nested: { prop: Output.of(b) }, + scalar: 1, + }); + expect(Object.keys(result).sort()).toEqual(["RA", "RB"]); + }); +}); + +describe("Output.toEnvKey / toUpper", () => { + it("uppercases strings", () => { + expect(Output.toUpper("hello")).toBe("HELLO"); + }); + + it("joins id + suffix and replaces dashes with underscores", () => { + expect(Output.toEnvKey("my-bucket", "name")).toBe("MY_BUCKET_NAME"); + expect(Output.toEnvKey("svc", "api-key")).toBe("SVC_API_KEY"); + }); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLBranch.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLBranch.test.ts new file mode 100644 index 00000000000..676c429a530 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLBranch.test.ts @@ -0,0 +1,630 @@ +import { adopt } from "@/AdoptPolicy"; +import * as Planetscale from "@/Planetscale"; +import * as RemovalPolicy from "@/RemovalPolicy.ts"; +import * as Test from "@/Test/Vitest"; +import * as ops from "@distilled.cloud/planetscale/Operations"; +import { describe, expect } from "@effect/vitest"; +import { Data, Schedule } from "effect"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import { MinimumLogLevel } from "effect/References"; + +const { test } = Test.make({ providers: Planetscale.providers() }); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +describe.skipIf(!process.env.PLANETSCALE_TEST)(() => { + test.provider( + "adopts existing branch when adopt is true", + (stack) => + Effect.gen(function* () { + const dbName = "alchemy-branch-adopt-true"; + const branchName = "adopted"; + + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + + return { database }; + }), + ); + + yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + "main", + ); + yield* deleteBranchIfExists(database.organization, dbName, branchName); + + yield* ops.createBranch({ + organization: database.organization, + database: dbName, + name: branchName, + parent_branch: "main", + }); + yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + branchName, + ); + + const { branch } = yield* stack + .deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("AdoptedBranch", { + name: branchName, + database, + parentBranch: "main", + isProduction: false, + }); + + return { database, branch }; + }), + ) + .pipe(adopt(true)); + + expect(branch).toMatchObject({ + name: branchName, + organization: database.organization, + database: dbName, + parentBranch: "main", + production: false, + createdAt: expect.any(String), + updatedAt: expect.any(String), + htmlUrl: expect.any(String), + region: { slug: expect.any(String) }, + }); + + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(dbName, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "errors on existing branch when adopt is false", + (stack) => + Effect.gen(function* () { + const dbName = "alchemy-branch-adopt-false"; + const branchName = "existing"; + + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + + return { database }; + }), + ); + + yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + "main", + ); + yield* deleteBranchIfExists(database.organization, dbName, branchName); + + yield* ops.createBranch({ + organization: database.organization, + database: dbName, + name: branchName, + parent_branch: "main", + }); + yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + branchName, + ); + + const exit = yield* stack + .deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("ExistingBranch", { + name: branchName, + database, + parentBranch: "main", + isProduction: false, + }); + + return { database, branch }; + }), + ) + .pipe(Effect.exit); + + yield* deleteBranchIfExists(database.organization, dbName, branchName); + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(dbName, database.organization); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const pretty = Cause.pretty(exit.cause); + expect(pretty).toContain("Cannot adopt resource"); + expect(pretty).toContain("Planetscale.MySQLBranch"); + } + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "can create branch with backup", + (stack) => + Effect.gen(function* () { + const dbName = "alchemy-branch-backup"; + const branchName = "restored"; + + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + + return { database }; + }), + ); + + yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + "main", + ); + yield* deleteBranchIfExists(database.organization, dbName, branchName); + + const backup = yield* ops.createBackup({ + organization: database.organization, + database: dbName, + branch: "main", + name: "alchemy-branch-backup-source", + retention_unit: "hour", + retention_value: 1, + }); + yield* waitForBackupSuccess( + database.organization, + dbName, + "main", + backup.id, + ); + + const { branch } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("RestoredBranch", { + name: branchName, + database, + parentBranch: "main", + isProduction: true, + backupId: backup.id, + clusterSize: "PS_10", + }); + + return { database, branch }; + }), + ); + + expect(branch).toMatchObject({ + name: branchName, + database: dbName, + parentBranch: "main", + production: true, + }); + + const live = yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + branchName, + ); + expect(live.name).toEqual(branchName); + expect(live.production).toBe(true); + expect(live.cluster_name).toEqual("PS_10"); + + yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("RestoredBranch", { + name: branchName, + database, + parentBranch: "main", + isProduction: false, + }); + + return { database, branch }; + }), + ); + + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(dbName, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "can enable and disable safe migrations", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, branch } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: true, + clusterSize: "PS_10", + safeMigrations: true, + }); + + return { database, branch }; + }), + ); + + const enabled = yield* ops.getBranch({ + organization: database.organization, + database: database.name, + branch: branch.name, + }); + expect(enabled.production).toBe(true); + expect(enabled.safe_migrations).toBe(true); + + const { updatedBranch } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + const updatedBranch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: true, + clusterSize: "PS_10", + safeMigrations: false, + }); + + return { database, updatedBranch }; + }), + ); + + expect(updatedBranch.name).toEqual(branch.name); + + const disabled = yield* ops.getBranch({ + organization: database.organization, + database: database.name, + branch: branch.name, + }); + expect(disabled.safe_migrations).toBe(false); + + yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: false, + }); + + return { database, branch }; + }), + ); + + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "can update cluster size", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, branch } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: true, + clusterSize: "PS_10", + }); + + return { database, branch }; + }), + ); + + const initial = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + branch.name, + ); + expect(initial.cluster_name).toEqual("PS_10"); + + const { resizedBranch } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + const resizedBranch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: true, + clusterSize: "PS_20", + }); + + return { database, resizedBranch }; + }), + ); + + expect(resizedBranch.name).toEqual(branch.name); + + const resized = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + branch.name, + ); + expect(resized.cluster_name).toEqual("PS_20"); + + yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + const branch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: false, + }); + + return { database, branch }; + }), + ); + + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "can create branch with Branch object as parent", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, parent, child } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + const parent = yield* Planetscale.MySQLBranch("ParentBranch", { + database, + parentBranch: "main", + isProduction: false, + }); + const child = yield* Planetscale.MySQLBranch("ChildBranch", { + database, + parentBranch: parent, + isProduction: false, + }); + + return { database, parent, child }; + }), + ); + + expect(child).toMatchObject({ + database: database.name, + parentBranch: parent.name, + production: false, + }); + + const live = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + child.name, + ); + expect(live.parent_branch).toEqual(parent.name); + + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "branch with RemovalPolicy.retain(true) should not be deleted via API", + (stack) => + Effect.gen(function* () { + const dbName = "alchemy-branch-retain"; + const branchName = "retained"; + + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }).pipe(RemovalPolicy.retain(true)); + + return { database }; + }), + ); + + yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + "main", + ); + yield* deleteBranchIfExists(database.organization, dbName, branchName); + + const { branch } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }).pipe(RemovalPolicy.retain(true)); + const branch = yield* Planetscale.MySQLBranch("RetainedBranch", { + name: branchName, + database, + parentBranch: "main", + isProduction: false, + }).pipe(RemovalPolicy.retain(true)); + + return { database, branch }; + }), + ); + + expect(branch.name).toEqual(branchName); + + yield* stack.destroy(); + + const live = yield* Planetscale.waitForBranchReady( + database.organization, + dbName, + branchName, + ); + expect(live.name).toEqual(branchName); + + yield* deleteBranchIfExists(database.organization, dbName, branchName); + yield* ops + .deleteDatabase({ + organization: database.organization, + database: dbName, + }) + .pipe(Effect.catchTag("NotFound", () => Effect.void)); + yield* waitForDatabaseToBeDeleted(dbName, database.organization); + }).pipe(logLevel), + 5_000_000, + ); +}); + +const deleteBranchIfExists = Effect.fn(function* ( + organization: string, + database: string, + branch: string, +) { + yield* ops + .deleteBranch({ organization, database, branch }) + .pipe(Effect.catchTag("NotFound", () => Effect.void)); + yield* waitForBranchToBeDeleted(organization, database, branch); +}); + +const waitForBranchToBeDeleted = Effect.fn(function* ( + organization: string, + database: string, + branch: string, +) { + yield* ops.getBranch({ organization, database, branch }).pipe( + Effect.flatMap(() => Effect.fail(new BranchStillExists())), + Effect.retry({ + while: (e): e is BranchStillExists => e instanceof BranchStillExists, + schedule: Schedule.exponential(100), + }), + Effect.catchTag("NotFound", () => Effect.void), + ); +}); + +const waitForDatabaseToBeDeleted = Effect.fn(function* ( + database: string, + organization: string, +) { + yield* ops + .getDatabase({ + organization, + database, + }) + .pipe( + Effect.flatMap(() => Effect.fail(new DatabaseStillExists())), + Effect.retry({ + while: (e): e is DatabaseStillExists => + e instanceof DatabaseStillExists, + schedule: Schedule.exponential(100), + }), + Effect.catchTag("NotFound", () => Effect.void), + ); +}); + +const waitForBackupSuccess = Effect.fn(function* ( + organization: string, + database: string, + branch: string, + id: string, +) { + return yield* ops.getBackup({ organization, database, branch, id }).pipe( + Effect.flatMap((backup) => { + switch (backup.state) { + case "success": + return Effect.succeed(backup); + case "failed": + case "canceled": + case "ignored": + return Effect.fail( + new BackupNotReady({ retryable: false, state: backup.state }), + ); + default: + return Effect.fail( + new BackupNotReady({ retryable: true, state: backup.state }), + ); + } + }), + Effect.retry({ + while: (e): e is BackupNotReady => + e instanceof BackupNotReady && e.retryable, + schedule: Schedule.exponential(1_000).pipe( + Schedule.both(Schedule.recurs(120)), + ), + }), + ); +}); + +class BranchStillExists extends Data.TaggedError("BranchStillExists") {} + +class DatabaseStillExists extends Data.TaggedError("DatabaseStillExists") {} + +class BackupNotReady extends Data.TaggedError("BackupNotReady")<{ + retryable: boolean; + state: string; +}> {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLDatabase.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLDatabase.test.ts new file mode 100644 index 00000000000..40c82777510 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLDatabase.test.ts @@ -0,0 +1,390 @@ +import { adopt } from "@/AdoptPolicy"; +import * as Planetscale from "@/Planetscale"; +import * as RemovalPolicy from "@/RemovalPolicy.ts"; +import * as Test from "@/Test/Vitest"; +import * as ops from "@distilled.cloud/planetscale/Operations"; +import { describe, expect } from "@effect/vitest"; +import { Data, Schedule } from "effect"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import { MinimumLogLevel } from "effect/References"; + +const { test } = Test.make({ providers: Planetscale.providers() }); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const fixturesDir = `${import.meta.dirname}/fixtures`; + +describe.skipIf(!process.env.PLANETSCALE_TEST)(() => { + test.provider("create database with minimal settings", (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase( + "MySQLDatabaseBasic", + { + clusterSize: "PS_10", + }, + ); + + return { + database, + }; + }), + ); + + expect(database).toMatchObject({ + kind: "mysql", + id: expect.any(String), + name: expect.any(String), + organization: expect.any(String), + state: expect.any(String), + defaultBranch: "main", + plan: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + htmlUrl: expect.any(String), + region: { + slug: expect.any(String), + }, + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + "main", + ); + + expect(branch.cluster_name).toEqual("PS_10"); + + yield* stack.destroy(); + }).pipe(logLevel), + ); + + test.provider("create, update, and delete database", (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase( + "MySQLDatabaseCRUD", + { + region: { + slug: "us-east", + }, + clusterSize: "PS_10", + defaultBranch: "main", + allowDataBranching: true, + automaticMigrations: true, + requireApprovalForDeploy: false, + restrictBranchRegion: true, + insightsRawQueries: true, + productionBranchWebConsole: true, + migrationFramework: "rails", + migrationTableName: "schema_migrations", + }, + ); + + return { + database, + }; + }), + ); + + expect(database).toMatchObject({ + kind: "mysql", + id: expect.any(String), + name: expect.any(String), + organization: expect.any(String), + state: expect.any(String), + plan: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + htmlUrl: expect.any(String), + region: { + slug: expect.any(String), + }, + clusterSize: "PS_10", + defaultBranch: "main", + allowDataBranching: true, + automaticMigrations: true, + requireApprovalForDeploy: false, + restrictBranchRegion: true, + insightsRawQueries: true, + productionBranchWebConsole: true, + migrationFramework: "rails", + migrationTableName: "schema_migrations", + }); + + const { updatedDatabase } = yield* stack.deploy( + Effect.gen(function* () { + const updatedDatabase = yield* Planetscale.MySQLDatabase( + "MySQLDatabaseCRUD", + { + clusterSize: "PS_20", + allowDataBranching: false, + automaticMigrations: true, + requireApprovalForDeploy: true, + restrictBranchRegion: false, + insightsRawQueries: false, + productionBranchWebConsole: false, + defaultBranch: "main", + migrationFramework: "django", + migrationTableName: "django_migrations", + }, + ); + + return { + updatedDatabase, + }; + }), + ); + + expect(updatedDatabase).toMatchObject({ + allowDataBranching: false, + automaticMigrations: true, + requireApprovalForDeploy: true, + restrictBranchRegion: false, + insightsRawQueries: false, + productionBranchWebConsole: false, + defaultBranch: "main", + migrationFramework: "django", + migrationTableName: "django_migrations", + }); + + const branch = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + "main", + ); + + expect(branch.cluster_name).toEqual("PS_20"); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + ); + + test.provider( + "creates non-main default branch if specified", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase( + "MySQLDatabaseCustomBranch", + { + clusterSize: "PS_10", + defaultBranch: "custom", + }, + ); + + return { + database, + }; + }), + ); + + expect(database).toMatchObject({ + kind: "mysql", + defaultBranch: "custom", + }); + + const branch = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + "custom", + ); + + expect(branch.name).toEqual("custom"); + expect(branch.parent_branch).toEqual("main"); + expect(branch.cluster_name).toEqual("PS_10"); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, // must wait on multiple resizes and branch creation + ); + + test.provider( + "applies migrations and import files", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const importFile = `${fixturesDir}/seed.sql`; + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase( + "MySQLDatabaseMigrations", + { + clusterSize: "PS_10", + migrationsDir: `${fixturesDir}/migrations`, + importFiles: [importFile], + }, + ); + + return { + database, + }; + }), + ); + + expect(database.migrationsTable).toEqual("planetscale_migrations"); + expect(database.migrationsHashes["0001_create_widgets.sql"]).toEqual( + expect.any(String), + ); + expect(database.importHashes[importFile]).toEqual(expect.any(String)); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "adopt with wrong kind should throw", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PgBaselineWrongKind", + { + region: { slug: "us-east" }, + clusterSize: "PS_10", + arch: "arm", // arm is slightly faster + }, + ); + + return { database }; + }), + ); + + yield* Planetscale.waitForDatabaseReady( + database.organization, + database.name, + ); + const exit = yield* Effect.exit( + stack + .deploy( + Effect.gen(function* () { + return yield* Planetscale.MySQLDatabase("MysqlWrongKind", { + name: database.name, + clusterSize: "PS_10", + }); + }), + ) + .pipe(adopt(true)), + ); + + expect(Exit.isFailure(exit)).toBe(true); + + if (Exit.isFailure(exit)) { + const pretty = Cause.pretty(exit.cause); + + expect(pretty).toContain("postgresql"); + expect(pretty).toContain("PostgresDatabase"); + } + yield* stack.destroy(); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "database with RemovalPolicy.retain(true) should not be deleted via API", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase( + "MySQLDatabaseRetainRemoval", + { + region: { slug: "us-east" }, + clusterSize: "PS_10", + }, + ).pipe(RemovalPolicy.retain(true)); + + return { + database, + }; + }), + ); + + // Verify database exists + const readyDatabase = yield* Planetscale.waitForDatabaseReady( + database.organization, + database.name, + ); + + expect(readyDatabase.name).toEqual(database.name); + + // When we call destroy, the database should NOT be deleted via API + yield* stack.destroy(); + + yield* Planetscale.waitForDatabaseReady( + database.organization, + database.name, + ); + + // Verify database still exists (was not deleted via API) + const live = yield* ops.getDatabase({ + organization: database.organization, + database: database.name, + }); + + // Database should still exist + expect(live.name).toEqual(database.name); + expect(live.state).toEqual("ready"); + expect(live.kind).toEqual("mysql"); + + // Clean up manually for the test + yield* ops + .deleteDatabase({ + organization: database.organization, + database: database.name, + }) + .pipe(Effect.catchTag("NotFound", () => Effect.void)); + }).pipe(logLevel), + 5_000_000, + ); +}); + +const waitForDatabaseToBeDeleted = Effect.fn(function* ( + database: string, + organization: string, +) { + yield* ops + .getDatabase({ + organization, + database, + }) + .pipe( + Effect.flatMap(() => Effect.fail(new DatabaseStillExists())), + Effect.retry({ + while: (e): e is DatabaseStillExists => + e instanceof DatabaseStillExists, + schedule: Schedule.exponential(100), + }), + Effect.catchTag("NotFound", () => Effect.void), + ); +}); + +class DatabaseStillExists extends Data.TaggedError("DatabaseStillExists") {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLPassword.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLPassword.test.ts new file mode 100644 index 00000000000..f4e50df05d8 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/MySQLPassword.test.ts @@ -0,0 +1,315 @@ +import * as Planetscale from "@/Planetscale"; +import * as RemovalPolicy from "@/RemovalPolicy.ts"; +import * as Test from "@/Test/Vitest"; +import * as ops from "@distilled.cloud/planetscale/Operations"; +import { describe, expect } from "@effect/vitest"; +import { Data, Schedule } from "effect"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import { MinimumLogLevel } from "effect/References"; + +const { test } = Test.make({ + providers: Planetscale.providers(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +describe.skipIf(!process.env.PLANETSCALE_TEST)(() => { + test.provider( + "create, update, and delete password", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, branch, password } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: false, + }); + + const password = yield* Planetscale.MySQLPassword("Password", { + database, + branch, + role: "reader", + }); + + return { database, branch, password }; + }), + ); + + expect(password).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + role: "reader", + host: expect.any(String), + username: expect.any(String), + organization: expect.any(String), + database: database.name, + branch: branch.name, + }); + + // Verify password was created by querying the API directly + const fetched = yield* ops.getPassword({ + organization: database.organization, + database: database.name, + branch: branch.name, + id: password.id, + }); + + expect(fetched.id).toEqual(password.id); + expect(fetched.name).toEqual(password.name); + expect(fetched.role).toEqual("reader"); + + // Update the password (only name and cidrs should trigger update, not replace) + const { updatedPassword } = yield* stack.deploy( + Effect.gen(function* () { + const sameDatabase = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + + const sameBranch = yield* Planetscale.MySQLBranch("Branch", { + database: sameDatabase, + parentBranch: "main", + isProduction: false, + }); + + const updatedPassword = yield* Planetscale.MySQLPassword( + "Password", + { + name: "test-updated-password-name", + database: sameDatabase.name, + branch: sameBranch.name, + role: "reader", + }, + ); + + return { updatedPassword }; + }), + ); + + expect(updatedPassword.id).toEqual(password.id); + expect(updatedPassword.name).not.toEqual(password.name); + + // Verify password was updated + const fetchedUpdated = yield* ops.getPassword({ + organization: database.organization, + database: database.name, + branch: branch.name, + id: updatedPassword.id, + }); + + expect(fetchedUpdated.id).toEqual(password.id); + expect(fetchedUpdated.name).toEqual(updatedPassword.name); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "password gets replaced when properties other than name and cidrs change", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, branch, password } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: false, + }); + + const password = yield* Planetscale.MySQLPassword("Password", { + database, + branch, + role: "reader", + ttl: 3600, + cidrs: ["0.0.0.0/0"], + }); + return { database, branch, password }; + }), + ); + + const originalId = password.id; + expect(password.role).toEqual("reader"); + expect(password.ttl).toEqual(3600); + + // Change role from reader -> writer (should trigger replace). + const { replacedPassword } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase("Database", { + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.MySQLBranch("Branch", { + database, + parentBranch: "main", + isProduction: false, + }); + + const replacedPassword = yield* Planetscale.MySQLPassword( + "Password", + { + database, + branch, + role: "writer", + ttl: 3600, + cidrs: ["0.0.0.0/0"], + }, + ); + return { replacedPassword }; + }), + ); + + // New ID due to replacement. + expect(replacedPassword.id).not.toEqual(originalId); + expect(replacedPassword.role).toEqual("writer"); + + // Old password should have been deleted as part of the replace. + const oldExit = yield* ops + .getPassword({ + organization: database.organization, + database: database.name, + branch: branch.name, + id: originalId, + }) + .pipe(Effect.exit); + expect(Exit.isFailure(oldExit)).toBe(true); + if (Exit.isFailure(oldExit)) { + expect(Cause.pretty(oldExit.cause)).toContain("NotFound"); + } + + // New password exists with the new role. + const newFetched = yield* ops.getPassword({ + organization: database.organization, + database: database.name, + branch: branch.name, + id: replacedPassword.id, + }); + expect(newFetched.id).toEqual(replacedPassword.id); + expect(newFetched.role).toEqual("writer"); + + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "password with RemovalPolicy.retain(true) should not be deleted via API", + (stack) => + Effect.gen(function* () { + const dbName = `alchemy-test-pwd-retain`; + const passwordName = `retain-password`; + + yield* stack.destroy(); + + const { database, password } = yield* stack.deploy( + Effect.gen(function* () { + // Retain the database too — otherwise deleting it would cascade + // to the password and we couldn't observe the retain behavior. + const database = yield* Planetscale.MySQLDatabase("Database", { + name: dbName, + clusterSize: "PS_10", + }).pipe(RemovalPolicy.retain(true)); + const password = yield* Planetscale.MySQLPassword("Password", { + name: passwordName, + database, + role: "reader", + }).pipe(RemovalPolicy.retain(true)); + return { database, password }; + }), + ); + + // Password exists post-deploy. + const fetched = yield* ops.getPassword({ + organization: database.organization, + database: database.name, + branch: "main", + id: password.id, + }); + expect(fetched.id).toEqual(password.id); + + // Destroy the stack — both retained, so neither should be removed. + yield* stack.destroy(); + + const { organization } = yield* Planetscale.Credentials; + + // Database should still exist and be ready. + const liveDb = yield* Planetscale.waitForDatabaseReady( + organization, + dbName, + ); + expect(liveDb.name).toEqual(dbName); + + // Password should still exist (was not deleted via API). + const stillExists = yield* ops.getPassword({ + organization, + database: dbName, + branch: "main", + id: password.id, + }); + expect(stillExists.id).toEqual(password.id); + expect(stillExists.name).toEqual(password.name); + + // Manual cleanup for the test. + yield* ops + .deletePassword({ + organization, + database: dbName, + branch: "main", + id: password.id, + }) + .pipe(Effect.catchTag("NotFound", () => Effect.void)); + + yield* ops + .deleteDatabase({ + organization, + database: dbName, + }) + .pipe(Effect.catchTag("NotFound", () => Effect.void)); + + yield* waitForDatabaseToBeDeleted(dbName, organization); + }).pipe(logLevel), + 5_000_000, + ); +}); + +const waitForDatabaseToBeDeleted = Effect.fn(function* ( + database: string, + organization: string, +) { + yield* ops + .getDatabase({ + organization, + database, + }) + .pipe( + Effect.flatMap(() => Effect.fail(new DatabaseStillExists())), + Effect.retry({ + while: (e): e is DatabaseStillExists => + e instanceof DatabaseStillExists, + schedule: Schedule.exponential(100), + }), + Effect.catchTag("NotFound", () => Effect.void), + ); +}); + +class DatabaseStillExists extends Data.TaggedError("DatabaseStillExists") {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/fixtures/migrations/0001_create_widgets.sql b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/fixtures/migrations/0001_create_widgets.sql new file mode 100644 index 00000000000..546a61da3d1 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/fixtures/migrations/0001_create_widgets.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS alchemy_mysql_widgets ( + id bigint NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/fixtures/seed.sql b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/fixtures/seed.sql new file mode 100644 index 00000000000..ff5bbb165df --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/MySQL/fixtures/seed.sql @@ -0,0 +1,3 @@ +INSERT INTO alchemy_mysql_widgets (id, name) +VALUES (1, 'seeded') +ON DUPLICATE KEY UPDATE name = VALUES(name); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/Hyperdrive.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/Hyperdrive.test.ts new file mode 100644 index 00000000000..7221c37d474 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/Hyperdrive.test.ts @@ -0,0 +1,113 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Planetscale from "@/Planetscale"; +import * as Test from "@/Test/Vitest"; +import { describe, expect } from "@effect/vitest"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import HyperdriveWorker from "./fixtures/hyperdrive-worker.ts"; +import type { Widget } from "./fixtures/schema.ts"; +import { Hyperdrive, PlanetscaleDb } from "./fixtures/Stack.ts"; + +const { test } = Test.make({ + providers: Layer.mergeAll(Cloudflare.providers(), Planetscale.providers()), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{ + status: number; + body: string; +}> {} + +describe.skipIf(!process.env.PLANETSCALE_TEST)(() => { + /** + * End-to-end: deploy a {@link Planetscale.PostgresDatabase} + branch + + * role, point a {@link Cloudflare.Hyperdrive} at the role's origin, and + * exercise the Drizzle Effect client over real Postgres via a Worker. + * + * Validates that: + * - migrations applied from the fixtures dir produce the expected table + * - `Cloudflare.Hyperdrive.bind(...) + Drizzle.postgres(...)` produces + * a working Effect-native client at runtime + * - INSERT / SELECT / DELETE round-trip through Hyperdrive to Planetscale + */ + test.provider( + "PostgresBranch + Hyperdrive + Drizzle round-trips through a Worker", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { worker } = yield* stack.deploy( + Effect.gen(function* () { + yield* PlanetscaleDb; + yield* Hyperdrive; + const worker = yield* HyperdriveWorker; + return { worker }; + }), + ); + + expect(worker.url).toBeTypeOf("string"); + const baseUrl = (worker.url as string).replace(/\/+$/, ""); + + // workers.dev edge takes a few seconds to start serving; retry the + // first request until we get a non-4xx. + const initial = yield* HttpClient.get(`${baseUrl}/widgets`).pipe( + Effect.flatMap((res) => + res.status === 200 + ? res.text.pipe(Effect.as(res)) + : res.text.pipe( + Effect.flatMap((body) => + Effect.fail( + new WorkerNotReady({ status: res.status, body }), + ), + ), + ), + ), + Effect.retry({ + while: (e): e is WorkerNotReady => + e instanceof WorkerNotReady && e.status >= 400 && e.status < 600, + schedule: Schedule.exponential("500 millis").pipe( + Schedule.both(Schedule.recurs(20)), + ), + }), + ); + expect(initial.status).toBe(200); + const initialBody = (yield* initial.json) as { widgets: Widget[] }; + expect(Array.isArray(initialBody.widgets)).toBe(true); + + const insertRes = yield* HttpClient.execute( + HttpClientRequest.post(`${baseUrl}/widgets`).pipe( + HttpClientRequest.bodyJsonUnsafe({ id: 1, name: "alpha" }), + ), + ); + expect(insertRes.status).toBe(200); + const insertBody = (yield* insertRes.json) as { widget: Widget }; + expect(insertBody.widget).toMatchObject({ id: 1, name: "alpha" }); + + const after = yield* HttpClient.get(`${baseUrl}/widgets`); + expect(after.status).toBe(200); + const afterBody = (yield* after.json) as { widgets: Widget[] }; + expect(afterBody.widgets.some((w) => w.id === 1)).toBe(true); + + const deleteRes = yield* HttpClient.execute( + HttpClientRequest.delete(`${baseUrl}/widgets/1`), + ); + expect(deleteRes.status).toBe(200); + + const final = yield* HttpClient.get(`${baseUrl}/widgets`); + const finalBody = (yield* final.json) as { widgets: Widget[] }; + expect(finalBody.widgets.some((w) => w.id === 1)).toBe(false); + + yield* stack.destroy(); + }).pipe(logLevel), + { timeout: 600_000 }, + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/PostgresDatabase.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/PostgresDatabase.test.ts new file mode 100644 index 00000000000..32cec30ada2 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/PostgresDatabase.test.ts @@ -0,0 +1,488 @@ +import { adopt } from "@/AdoptPolicy"; +import * as Planetscale from "@/Planetscale"; +import * as RemovalPolicy from "@/RemovalPolicy.ts"; +import * as Test from "@/Test/Vitest"; +import * as ops from "@distilled.cloud/planetscale/Operations"; +import { describe, expect } from "@effect/vitest"; +import { Data, Schedule } from "effect"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import { MinimumLogLevel } from "effect/References"; + +const { test } = Test.make({ providers: Planetscale.providers() }); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +const fixturesDir = `${import.meta.dirname}/fixtures`; + +describe.skipIf(!process.env.PLANETSCALE_TEST)(() => { + test.provider( + "create database with minimal settings", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseBasic", + { + clusterSize: "PS_10", + }, + ); + + return { + database, + }; + }), + ); + + expect(database).toMatchObject({ + kind: "postgresql", + id: expect.any(String), + name: expect.any(String), + organization: expect.any(String), + state: expect.any(String), + defaultBranch: "main", + plan: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + htmlUrl: expect.any(String), + region: { + slug: expect.any(String), + }, + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + "main", + ); + + expect(branch.cluster_name).toEqual("PS_10_AWS_X86"); + + yield* stack.destroy(); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "create, update, and delete database", + (stack) => + Effect.gen(function* () { + const name = `alchemy-test-postgresql-crud`; + + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseCRUD", + { + name, + region: { + slug: "us-east", + }, + clusterSize: "PS_10", + defaultBranch: "main", + requireApprovalForDeploy: false, + restrictBranchRegion: true, + productionBranchWebConsole: true, + }, + ); + + return { + database, + }; + }), + ); + + expect(database).toMatchObject({ + kind: "postgresql", + id: expect.any(String), + name, + organization: expect.any(String), + state: expect.any(String), + plan: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + htmlUrl: expect.any(String), + region: { + slug: expect.any(String), + }, + clusterSize: "PS_10", + defaultBranch: "main", + requireApprovalForDeploy: false, + restrictBranchRegion: true, + productionBranchWebConsole: true, + }); + + const { updatedDatabase } = yield* stack.deploy( + Effect.gen(function* () { + const updatedDatabase = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseCRUD", + { + name, + clusterSize: "PS_20", + requireApprovalForDeploy: true, + restrictBranchRegion: false, + productionBranchWebConsole: false, + defaultBranch: "main", + }, + ); + + return { + updatedDatabase, + }; + }), + ); + + expect(updatedDatabase).toMatchObject({ + name, + requireApprovalForDeploy: true, + restrictBranchRegion: false, + productionBranchWebConsole: false, + defaultBranch: "main", + }); + + const branch = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + "main", + ); + + expect(branch.cluster_name).toEqual("PS_20_AWS_X86"); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "creates non-main default branch if specified", + (stack) => + Effect.gen(function* () { + const name = `alchemy-test-postgresql-custom-branch`; + const defaultBranch = "custom"; + + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseCustomBranch", + { + name, + clusterSize: "PS_10", + defaultBranch, + }, + ); + + return { + database, + }; + }), + ); + + expect(database).toMatchObject({ + name, + kind: "postgresql", + defaultBranch, + }); + + const branch = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + defaultBranch, + ); + + expect(branch.name).toEqual(defaultBranch); + expect(branch.parent_branch).toEqual("main"); + expect(branch.cluster_name).toEqual("PS_10_AWS_X86"); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, // must wait on multiple resizes and branch creation + ); + + test.provider( + "create database with arm arch", + (stack) => + Effect.gen(function* () { + const name = `alchemy-test-postgresql-basic`; + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseBasic", + { + name, + clusterSize: "PS_10", + arch: "arm", + }, + ); + + return { + database, + }; + }), + ); + + expect(database).toMatchObject({ + kind: "postgresql", + id: expect.any(String), + name, + arch: "arm", + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.waitForBranchReady( + database.organization, + database.name, + "main", + ); + + expect(branch.cluster_name).toEqual("PS_10_AWS_ARM"); + + yield* stack.destroy(); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "applies migrations and import files", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const importFile = `${fixturesDir}/seed.sql`; + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseMigrations", + { + clusterSize: "PS_10", + migrationsDir: `${fixturesDir}/migrations`, + importFiles: [importFile], + }, + ); + + return { + database, + }; + }), + ); + + expect(database.migrationsTable).toEqual("planetscale_migrations"); + expect(database.migrationsHashes["0001_create_widgets.sql"]).toEqual( + expect.any(String), + ); + expect(database.importHashes[importFile]).toEqual(expect.any(String)); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "adopt with wrong kind should throw", + (stack) => + Effect.gen(function* () { + const name = `alchemy-test-postgresql-kind`; + + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.MySQLDatabase( + "MySQLBaselineWrongKind", + { + name, + region: { slug: "us-east" }, + clusterSize: "PS_10", + }, + ); + + return { database }; + }), + ); + + yield* Planetscale.waitForDatabaseReady(database.organization, name); + + const exit = yield* Effect.exit( + stack + .deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresWrongKind", + { + name, + clusterSize: "PS_10", + }, + ); + + return { database }; + }), + ) + .pipe(adopt(true)), + ); + + expect(Exit.isFailure(exit)).toBe(true); + + if (Exit.isFailure(exit)) { + const pretty = Cause.pretty(exit.cause); + expect(pretty).toContain("mysql"); + expect(pretty).toContain("MySQLDatabase"); + } + + yield* stack.destroy(); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "adopt with wrong arch should trigger replace", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + // Create a database with arm + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseWrongArch", + { + arch: "arm", + clusterSize: "PS_10", + }, + ); + + return { + database, + }; + }), + ); + + expect(database.arch).toEqual("arm"); + + // Now try to adopt it with a different arch — should replace + const { newDatabase } = yield* stack.deploy( + Effect.gen(function* () { + const newDatabase = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseWrongArch", + { + arch: "x86", + clusterSize: "PS_10", + }, + ); + return { + newDatabase, + }; + }), + ); + + expect(newDatabase.id).not.toBe(database.id); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + yield* waitForDatabaseToBeDeleted( + newDatabase.name, + newDatabase.organization, + ); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "database with RemovalPolicy.retain(true) should not be deleted via API", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase( + "PostgresDatabaseRetainRemoval", + { + region: { slug: "us-east" }, + clusterSize: "PS_10", + }, + ).pipe(RemovalPolicy.retain(true)); + + return { + database, + }; + }), + ); + + // Verify database exists + yield* Planetscale.waitForDatabaseReady( + database.organization, + database.name, + ); + + // When we call destroy, the database should NOT be deleted via API + yield* stack.destroy(); + + yield* Planetscale.waitForDatabaseReady( + database.organization, + database.name, + ); + + // Verify database still exists (was not deleted via API) + const live = yield* ops.getDatabase({ + organization: database.organization, + database: database.name, + }); + + // Database should still exist + expect(live.name).toEqual(database.name); + expect(live.state).toEqual("ready"); + expect(live.kind).toEqual("postgresql"); + + // Clean up manually for the test + yield* ops + .deleteDatabase({ + organization: database.organization, + database: database.name, + }) + .pipe(Effect.catchTag("NotFound", () => Effect.void)); + }).pipe(logLevel), + 5_000_000, + ); +}); + +const waitForDatabaseToBeDeleted = Effect.fn(function* ( + database: string, + organization: string, +) { + yield* ops + .getDatabase({ + organization, + database, + }) + .pipe( + Effect.flatMap(() => Effect.fail(new DatabaseStillExists())), + Effect.retry({ + while: (e): e is DatabaseStillExists => + e instanceof DatabaseStillExists, + schedule: Schedule.exponential(100), + }), + Effect.catchTag("NotFound", () => Effect.void), + ); +}); + +class DatabaseStillExists extends Data.TaggedError("DatabaseStillExists") {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/PostgresRole.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/PostgresRole.test.ts new file mode 100644 index 00000000000..15ff2b79904 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/PostgresRole.test.ts @@ -0,0 +1,439 @@ +import * as Planetscale from "@/Planetscale"; +import * as RemovalPolicy from "@/RemovalPolicy.ts"; +import * as Test from "@/Test/Vitest"; +import * as ops from "@distilled.cloud/planetscale/Operations"; +import { describe, expect } from "@effect/vitest"; +import { Redacted } from "effect"; +import * as Cause from "effect/Cause"; +import * as Data from "effect/Data"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; + +const { test } = Test.make({ providers: Planetscale.providers() }); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); +describe.skipIf(!process.env.PLANETSCALE_TEST)(() => { + test.provider( + "default role - create, duplicate fails, forceReset returns new id", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + // First: create default role, expect success + const { database, role1 } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role1 = yield* Planetscale.PostgresDefaultRole("Role1", { + database, + }); + + return { role1, database }; + }), + ); + + expect(role1).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + host: expect.any(String), + username: expect.any(String), + password: expect.any(Object), + databaseName: "postgres", + branch: "main", + organization: database.organization, + }); + + // Second: create again without forceReset — should fail (default already exists) + const exit = yield* stack + .deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role2 = yield* Planetscale.PostgresDefaultRole("Role2", { + database, + }); + + return { role2 }; + }), + ) + .pipe(Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + + if (Exit.isFailure(exit)) { + expect(Cause.pretty(exit.cause)).toMatch( + /Default role already exists.*Use forceReset/, + ); + } + + // Third: create with forceReset — should succeed and return a different role id + const { role3 } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role3 = yield* Planetscale.PostgresDefaultRole("Role3", { + database, + forceReset: true, + }); + + return { role3, database }; + }), + ); + + expect(role3).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + host: expect.any(String), + username: expect.any(String), + password: expect.any(Object), + databaseName: "postgres", + branch: "main", + organization: database.organization, + }); + + // the default role ID is the same, but the password is different + expect(Redacted.value(role3.password)).not.toEqual( + Redacted.value(role1.password), + ); + + yield* stack.destroy(); + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "create and delete role", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + // Create a role + const { database, role } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role = yield* Planetscale.PostgresRole("Role", { + database, + // Empty array means no permissions, which is fine for testing. + inheritedRoles: [], + }); + + return { database, role }; + }), + ); + + expect(role).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + host: expect.any(String), + username: expect.any(String), + password: expect.any(Object), + }); + + // Update role with different ttl (should trigger replacement) + const { updatedRole } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const updatedRole = yield* Planetscale.PostgresRole("Role", { + database, + ttl: 3600, + inheritedRoles: [], + }); + + return { database, updatedRole }; + }), + ); + + expect(role.id).not.toEqual(updatedRole.id); + expect(updatedRole.ttl).toEqual(3600); + + const found = yield* ops + .getRole({ + id: role.id, + database: database.name, + organization: database.organization, + branch: "main", + }) + .pipe( + Effect.map(() => true), + Effect.catchTag("NotFound", () => Effect.succeed(false)), + ); + + expect(found).toBe(false); + + const updatedRoleFromApi = yield* ops.getRole({ + id: updatedRole.id, + database: database.name, + organization: database.organization, + branch: "main", + }); + + expect(updatedRoleFromApi.ttl).toEqual(3600); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "role gets replaced when properties change", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, role1 } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role1 = yield* Planetscale.PostgresRole("RoleReplace", { + database: database, + inheritedRoles: ["pg_read_all_data"], + ttl: 3600, + }); + + return { database, role1 }; + }), + ); + + expect(role1).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + inheritedRoles: ["pg_read_all_data"], + }); + + const originalId = role1.id; + const originalName = role1.name; + + const { role2 } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role2 = yield* Planetscale.PostgresRole("RoleReplace", { + database: database, + inheritedRoles: ["postgres"], + ttl: 7200, + }); + + return { role2 }; + }), + ); + + expect(role2).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + inheritedRoles: ["postgres"], + }); + + expect(role2.id).not.toEqual(originalId); + expect(role2.name).not.toEqual(originalName); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "role with RemovalPolicy.retain(true) should not be deleted via API", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { organization } = yield* Planetscale.Credentials; + + const { database, role } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }).pipe(RemovalPolicy.retain(true)); + const role = yield* Planetscale.PostgresRole("RoleRetainRemoval", { + database, + inheritedRoles: ["postgres"], + }).pipe(RemovalPolicy.retain(true)); + + return { database, role }; + }), + ); + + expect(role).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + database: database.name, + inheritedRoles: ["postgres"], + }); + + yield* stack.destroy(); + + const liveRole = yield* ops + .getRole({ + organization, + database: database.name, + branch: "main", + id: role.id, + }) + .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))); + + expect(liveRole).toBeDefined(); + expect(liveRole?.id).toEqual(role.id); + + // deleting the db takes care of deleting the role + yield* ops + .deleteDatabase({ + organization, + database: database.name, + }) + .pipe(Effect.catchTag("NotFound", () => Effect.void)); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "role update: successor is updatable without replacement", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, role1 } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role1 = yield* Planetscale.PostgresRole("RoleSuccessor", { + database, + inheritedRoles: ["postgres"], + successor: "postgres", + }); + + return { database, role1 }; + }), + ); + + expect(role1).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + successor: "postgres", + }); + + const originalId = role1.id; + + const { role2 } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + const role2 = yield* Planetscale.PostgresRole("RoleSuccessor", { + database, + inheritedRoles: ["postgres"], + successor: "pg_read_all_data", + }); + + return { role2 }; + }), + ); + + expect(role2).toMatchObject({ + id: originalId, + successor: "pg_read_all_data", + }); + + expect(role2.id).toEqual(originalId); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); + + test.provider( + "role with custom branch", + (stack) => + Effect.gen(function* () { + yield* stack.destroy(); + + const { database, branch, role } = yield* stack.deploy( + Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("Database", { + clusterSize: "PS_10", + arch: "arm", + }); + + const branch = yield* Planetscale.PostgresBranch("CustomBranch", { + database, + }); + + const role = yield* Planetscale.PostgresRole("RoleCustomBranch", { + database, + branch, + inheritedRoles: ["postgres"], + }); + + return { database, branch, role }; + }), + ); + + expect(role).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + database: database.name, + branch: branch.name, + inheritedRoles: ["postgres"], + }); + + yield* stack.destroy(); + + yield* waitForDatabaseToBeDeleted(database.name, database.organization); + }).pipe(logLevel), + 5_000_000, + ); +}); +const waitForDatabaseToBeDeleted = Effect.fn(function* ( + database: string, + organization: string, +) { + yield* ops + .getDatabase({ + organization, + database, + }) + .pipe( + Effect.flatMap(() => Effect.fail(new DatabaseStillExists())), + Effect.retry({ + while: (e): e is DatabaseStillExists => + e instanceof DatabaseStillExists, + schedule: Schedule.exponential(100), + }), + Effect.catchTag("NotFound", () => Effect.void), + ); +}); + +class DatabaseStillExists extends Data.TaggedError("DatabaseStillExists") {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/Stack.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/Stack.ts new file mode 100644 index 00000000000..1b340daeb2e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/Stack.ts @@ -0,0 +1,39 @@ +import * as Cloudflare from "@/Cloudflare/index.ts"; +import * as Planetscale from "@/Planetscale/index.ts"; +import * as Effect from "effect/Effect"; + +/** + * Shared Planetscale + Cloudflare wiring used by the Hyperdrive + * fixture worker. A long-lived staging Postgres database (named + * deterministically so reruns adopt the same resource) owns a feature + * branch + role; Hyperdrive points at `role.origin` so the worker can + * connect over Postgres-on-PSBouncer. + */ +export const PlanetscaleDb = Effect.gen(function* () { + const database = yield* Planetscale.PostgresDatabase("HyperdriveTestDb", { + name: "alchemy-postgres-hyperdrive", + region: { slug: "us-east" }, + clusterSize: "PS_10", + }); + + const branch = yield* Planetscale.PostgresBranch("HyperdriveTestBranch", { + database, + migrationsDir: + "./packages/alchemy/test/Planetscale/Postgres/fixtures/migrations", + }); + + const role = yield* Planetscale.PostgresRole("HyperdriveTestRole", { + database, + branch, + inheritedRoles: ["postgres"], + }); + + return { database, branch, role }; +}); + +export const Hyperdrive = Effect.gen(function* () { + const { role } = yield* PlanetscaleDb; + return yield* Cloudflare.Hyperdrive("HyperdriveTestEdge", { + origin: role.origin, + }); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/hyperdrive-worker.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/hyperdrive-worker.ts new file mode 100644 index 00000000000..e3cd48e2d6c --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/hyperdrive-worker.ts @@ -0,0 +1,73 @@ +import * as Cloudflare from "@/Cloudflare/index.ts"; +import * as Drizzle from "@/Drizzle/index.ts"; +import { eq } from "drizzle-orm"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Hyperdrive } from "./Stack.ts"; +import { relations, Widgets } from "./schema.ts"; + +/** + * Worker fixture that binds a Cloudflare Hyperdrive (pointed at a + * Planetscale Postgres role) and exercises Drizzle's Effect-native + * client. Mirrors the cloudflare-planetscale-drizzle example but lives + * inside the package so the runtime path is covered by an integration + * test next to the resource it exercises. + */ +export default class HyperdriveWorker extends Cloudflare.Worker()( + "PlanetscaleHyperdriveWorker", + { + main: import.meta.filename, + subdomain: { enabled: true }, + compatibility: { date: "2024-09-23", flags: ["nodejs_compat"] }, + }, + Effect.gen(function* () { + const conn = yield* Cloudflare.Hyperdrive.bind(Hyperdrive); + const db = yield* Drizzle.postgres(conn.connectionString, { relations }); + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.url, "http://x"); + + if (request.method === "GET" && url.pathname === "/widgets") { + const widgets = yield* db.select().from(Widgets); + return yield* HttpServerResponse.json({ widgets }); + } + + if (request.method === "POST" && url.pathname === "/widgets") { + const body = (yield* request.json) as { id: number; name: string }; + const [inserted] = yield* db + .insert(Widgets) + .values({ id: body.id, name: body.name }) + .onConflictDoUpdate({ + target: Widgets.id, + set: { name: body.name }, + }) + .returning(); + return yield* HttpServerResponse.json({ widget: inserted }); + } + + const idMatch = url.pathname.match(/^\/widgets\/(\d+)$/); + if (request.method === "DELETE" && idMatch) { + const id = Number(idMatch[1]); + const [deleted] = yield* db + .delete(Widgets) + .where(eq(Widgets.id, id)) + .returning(); + return yield* HttpServerResponse.json({ widget: deleted ?? null }); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }).pipe( + Effect.catch((cause: any) => + HttpServerResponse.json( + { ok: false, error: String(cause) }, + { status: 500 }, + ), + ), + ), + }; + }).pipe(Effect.provide(Layer.mergeAll(Cloudflare.HyperdriveBindingLive))), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/migrations/0001_create_widgets.sql b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/migrations/0001_create_widgets.sql new file mode 100644 index 00000000000..173c6be07b4 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/migrations/0001_create_widgets.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS alchemy_postgres_widgets ( + id integer PRIMARY KEY, + name text NOT NULL +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/schema.ts b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/schema.ts new file mode 100644 index 00000000000..230c14296ae --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/schema.ts @@ -0,0 +1,10 @@ +import { defineRelations } from "drizzle-orm"; +import { integer, pgTable, text } from "drizzle-orm/pg-core"; + +export const Widgets = pgTable("alchemy_postgres_widgets", { + id: integer("id").primaryKey(), + name: text("name").notNull(), +}); +export type Widget = typeof Widgets.$inferSelect; + +export const relations = defineRelations({ Widgets }, () => ({})); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/seed.sql b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/seed.sql new file mode 100644 index 00000000000..8fa089555f3 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Planetscale/Postgres/fixtures/seed.sql @@ -0,0 +1,3 @@ +INSERT INTO alchemy_postgres_widgets (id, name) +VALUES (1, 'seeded') +ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name; diff --git a/.repos/alchemy-effect/packages/alchemy/test/State/Sync.test.ts b/.repos/alchemy-effect/packages/alchemy/test/State/Sync.test.ts new file mode 100644 index 00000000000..4b4b26d8330 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/State/Sync.test.ts @@ -0,0 +1,123 @@ +import { + InMemoryService, + syncState, + type ResourceState, + type StateService, +} from "@/State"; +import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; + +describe("syncState", () => { + it.effect( + "copies source resources and overwrites matching destination resources", + () => + Effect.gen(function* () { + const sourceA = resource("resource-a", { value: "source-a" }); + const sourceB = resource("resource-b", { value: "source-b" }); + const destinationA = resource("resource-a", { value: "destination-a" }); + + const source = yield* InMemoryService({ + app: { + dev: { + "resource-a": sourceA, + "resource-b": sourceB, + }, + }, + }); + const destination = yield* InMemoryService({ + app: { + dev: { + "resource-a": destinationA, + }, + }, + }); + + yield* syncState(source, destination); + + yield* expectStage(destination, "app", "dev", { + "resource-a": sourceA, + "resource-b": sourceB, + }); + }), + ); + + it.effect( + "deletes resources from destination when they are absent from source", + () => + Effect.gen(function* () { + const source = yield* InMemoryService({ + app: { + dev: { + "resource-a": resource("resource-a", { value: "source-a" }), + }, + }, + }); + const destination = yield* InMemoryService({ + app: { + dev: { + "resource-a": resource("resource-a", { value: "destination-a" }), + "resource-b": resource("resource-b", { value: "destination-b" }), + }, + prod: { + "resource-c": resource("resource-c", { value: "destination-c" }), + }, + }, + oldApp: { + dev: { + "resource-d": resource("resource-d", { value: "destination-d" }), + }, + }, + }); + + yield* syncState(source, destination); + + yield* expectStage(destination, "app", "dev", { + "resource-a": resource("resource-a", { value: "source-a" }), + }); + yield* expectStage(destination, "app", "prod", {}); + yield* expectStage(destination, "oldApp", "dev", {}); + expect(yield* destination.listStacks()).toEqual(["app"]); + }), + ); +}); + +const resource = ( + fqn: string, + attr: Record, +): ResourceState => ({ + resourceType: "test:resource", + namespace: undefined, + fqn, + logicalId: fqn, + instanceId: `instance-${fqn}`, + providerVersion: 1, + status: "created", + downstream: [], + bindings: [], + props: {}, + attr, +}); + +const listStage = Effect.fnUntraced(function* ( + state: StateService, + stack: string, + stage: string, +) { + const fqns = yield* state.list({ stack, stage }); + const entries = yield* Effect.forEach( + fqns, + Effect.fnUntraced(function* (fqn) { + return [fqn, yield* state.get({ stack, stage, fqn })] as const; + }), + ); + return Object.fromEntries(entries); +}); + +const expectStage = Effect.fnUntraced(function* ( + state: StateService, + stack: string, + stage: string, + expected: Record, +) { + expect(yield* listStage(state, stack, stage)).toEqual(expected); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/Util/data.test.ts b/.repos/alchemy-effect/packages/alchemy/test/Util/data.test.ts new file mode 100644 index 00000000000..200db17b9ee --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/Util/data.test.ts @@ -0,0 +1,63 @@ +import { + isPlainObject, + stripNullFields, + stripUndefinedFields, + unwrapRedacted, +} from "@/Util/data"; +import * as Redacted from "effect/Redacted"; +import { describe, expect, test } from "vitest"; + +describe("data utilities", () => { + test("isPlainObject accepts object literals", () => { + expect(isPlainObject({})).toBe(true); + }); + + test("isPlainObject rejects arrays and object instances", () => { + expect(isPlainObject([])).toBe(false); + expect(isPlainObject(Object.create(null))).toBe(false); + expect(isPlainObject(new Date())).toBe(false); + expect(isPlainObject(Redacted.make("secret"))).toBe(false); + }); + + test("stripNullFields removes nulls recursively from arrays and records", () => { + expect( + stripNullFields({ + a: null, + b: undefined, + c: [{ d: null, e: 1 }], + }), + ).toEqual({ + b: undefined, + c: [{ e: 1 }], + }); + }); + + test("stripUndefinedFields removes undefined recursively from arrays and records", () => { + expect( + stripUndefinedFields({ + a: null, + b: undefined, + c: [{ d: undefined, e: 1 }], + }), + ).toEqual({ + a: null, + c: [{ e: 1 }], + }); + }); + + test("unwrapRedacted unwraps only arrays and plain records recursively", () => { + const date = new Date("2026-05-20T00:00:00.000Z"); + + expect( + unwrapRedacted({ + value: Redacted.make("secret"), + nested: [Redacted.make("nested")], + date, + }), + ).toEqual({ + value: "secret", + nested: ["nested"], + date, + }); + }); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/action.test.ts b/.repos/alchemy-effect/packages/alchemy/test/action.test.ts new file mode 100644 index 00000000000..2d930418471 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/action.test.ts @@ -0,0 +1,459 @@ +import { Action } from "@/Action"; +import * as Plan from "@/Plan"; +import * as Stack from "@/Stack"; +import { Stage } from "@/Stage"; +import { + InMemoryService, + inMemoryState, + State, + type ActionState, + type RanActionState, +} from "@/State"; +import * as Test from "@/Test/Vitest"; +import { describe, expect } from "@effect/vitest"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Ref from "effect/Ref"; +import { Bucket, TestLayers } from "./test.resources"; + +const TEST_STACK = "task-test"; +const TEST_STAGE = "test"; + +// Fresh in-memory state per test so persisted task rows don't leak between +// runs in the same file. +const freshState = () => + Layer.effect( + State, + Effect.sync(() => InMemoryService({})), + ); + +const { test } = Test.make({ + providers: TestLayers(), + state: freshState(), +}); + +const resolveStackId = Effect.gen(function* () { + const ambient = yield* Effect.serviceOption(Stack.Stack); + return Option.match(ambient, { + onNone: () => ({ name: TEST_STACK, stage: TEST_STAGE }), + onSome: (s) => ({ name: s.name, stage: s.stage }), + }); +}); + +const seed = (rows: Record) => + Effect.gen(function* () { + const { name, stage } = yield* resolveStackId; + const state = yield* yield* State; + for (const [fqn, value] of Object.entries(rows)) { + yield* state.set({ stack: name, stage, fqn, value }); + } + }); + +const makePlan = ( + effect: Effect.Effect, + options?: Plan.MakePlanOptions, +): Effect.Effect, Err, State> => + // @ts-expect-error - Stack.make's typing erases R unsoundly here + Effect.gen(function* () { + const { name, stage } = yield* resolveStackId; + // @ts-expect-error + return yield* effect.pipe( + // @ts-expect-error + Stack.make({ + name, + providers: Layer.empty, + state: inMemoryState(), + }), + Effect.provideService(Stage, stage), + Effect.flatMap((stackSpec: any) => Plan.make(stackSpec, options)), + Effect.provide(TestLayers()), + ); + }); + +// ── Plan tests ──────────────────────────────────────────────────────────── + +describe("Plan", () => { + test( + "first-time task -> run", + Effect.gen(function* () { + const Sync = Action("Sync", (input: { table: string }) => + Effect.succeed({ rows: 1, table: input.table }), + ); + + const plan = yield* Effect.gen(function* () { + return yield* Sync({ table: "users" }); + }).pipe(makePlan); + + expect(plan.actions.Sync).toMatchObject({ + kind: "action", + action: "run", + state: undefined, + forced: false, + }); + expect(plan.actions.Sync.def.LogicalId).toBe("Sync"); + }), + ); + + test( + "same input hash -> noop (skip)", + Effect.gen(function* () { + const Sync = Action("Sync", (_: { table: string }) => + Effect.succeed({ rows: 1 }), + ); + + // Pre-seed a `ran` row with a hash that matches { table: "users" }. + const { hashInput } = yield* Effect.promise(() => import("@/Util/hash")); + const inputHash = yield* hashInput({ table: "users" }); + yield* seed({ + Sync: { + kind: "action", + status: "ran", + fqn: "Sync", + logicalId: "Sync", + namespace: undefined, + actionType: "Sync", + inputHash, + input: { table: "users" }, + output: { rows: 1 }, + downstream: [], + } satisfies RanActionState, + }); + + const plan = yield* Effect.gen(function* () { + return yield* Sync({ table: "users" }); + }).pipe(makePlan); + + expect(plan.actions.Sync.action).toBe("noop"); + }), + ); + + test( + "changed input hash -> run", + Effect.gen(function* () { + const Sync = Action("Sync", (_: { table: string }) => + Effect.succeed({ rows: 1 }), + ); + + const { hashInput } = yield* Effect.promise(() => import("@/Util/hash")); + const oldHash = yield* hashInput({ table: "users" }); + yield* seed({ + Sync: { + kind: "action", + status: "ran", + fqn: "Sync", + logicalId: "Sync", + namespace: undefined, + actionType: "Sync", + inputHash: oldHash, + input: { table: "users" }, + output: { rows: 1 }, + downstream: [], + } satisfies RanActionState, + }); + + const plan = yield* Effect.gen(function* () { + return yield* Sync({ table: "orders" }); + }).pipe(makePlan); + + expect(plan.actions.Sync.action).toBe("run"); + }), + ); + + test( + "force flips noop -> run", + Effect.gen(function* () { + const Sync = Action("Sync", (_: { table: string }) => + Effect.succeed({ rows: 1 }), + ); + + const { hashInput } = yield* Effect.promise(() => import("@/Util/hash")); + const inputHash = yield* hashInput({ table: "users" }); + yield* seed({ + Sync: { + kind: "action", + status: "ran", + fqn: "Sync", + logicalId: "Sync", + namespace: undefined, + actionType: "Sync", + inputHash, + input: { table: "users" }, + output: { rows: 1 }, + downstream: [], + } satisfies RanActionState, + }); + + const plan = yield* Effect.gen(function* () { + return yield* Sync({ table: "users" }); + }).pipe((eff) => makePlan(eff, { force: true })); + + expect(plan.actions.Sync.action).toBe("run"); + expect((plan.actions.Sync as Plan.ActionRun).forced).toBe(true); + }), + ); + + test( + "task removed from stack -> taskDeletions", + Effect.gen(function* () { + const { hashInput } = yield* Effect.promise(() => import("@/Util/hash")); + const inputHash = yield* hashInput({ table: "users" }); + yield* seed({ + Sync: { + kind: "action", + status: "ran", + fqn: "Sync", + logicalId: "Sync", + namespace: undefined, + actionType: "Sync", + inputHash, + input: { table: "users" }, + output: { rows: 1 }, + downstream: [], + } satisfies RanActionState, + }); + + // The new stack has no tasks at all. + const plan = yield* Effect.gen(function* () { + return undefined; + }).pipe(makePlan); + + expect(plan.actionDeletions.Sync).toMatchObject({ + kind: "action", + action: "delete", + }); + expect(plan.actions.Sync).toBeUndefined(); + }), + ); + + test( + "resource depends on task: task is upstream of resource", + Effect.gen(function* () { + const Compute = Action("Compute", (_: {}) => + Effect.succeed({ value: "computed" }), + ); + + const plan = yield* Effect.gen(function* () { + const computed = yield* Compute({}); + const bucket = yield* Bucket("MyBucket", { name: computed.value }); + return bucket; + }).pipe(makePlan); + + // Action is run (first time), bucket is created and lists Compute as upstream. + expect(plan.actions.Compute.action).toBe("run"); + expect(plan.actions.Compute.downstream).toContain("MyBucket"); + }), + ); + + test( + "task depends on resource: resource is upstream of task", + Effect.gen(function* () { + const Sync = Action("Sync", (_: { name: string }) => + Effect.succeed({ ok: true }), + ); + + const plan = yield* Effect.gen(function* () { + const bucket = yield* Bucket("MyBucket", { name: "b" }); + return yield* Sync({ name: bucket.name }); + }).pipe(makePlan); + + expect(plan.resources.MyBucket.action).toBe("create"); + expect(plan.actions.Sync.action).toBe("run"); + // MyBucket's downstream includes the task FQN. + expect(plan.resources.MyBucket.downstream).toContain("Sync"); + }), + ); + + test( + "explicit logical id allows multiple instances", + Effect.gen(function* () { + const Sync = Action("Sync", (_: { which: string }) => + Effect.succeed({ ok: true }), + ); + + const plan = yield* Effect.gen(function* () { + yield* Sync("nightly", { which: "n" }); + yield* Sync("hourly", { which: "h" }); + }).pipe(makePlan); + + expect(plan.actions.nightly.action).toBe("run"); + expect(plan.actions.hourly.action).toBe("run"); + expect(plan.actions.Sync).toBeUndefined(); + }), + ); +}); + +// ── Apply tests ─────────────────────────────────────────────────────────── + +describe("Apply", () => { + test.provider("first run invokes body and persists ran state", (stack) => + Effect.gen(function* () { + const counter = yield* Ref.make(0); + const Sync = Action("Sync", (input: { n: number }) => + Effect.gen(function* () { + yield* Ref.update(counter, (c) => c + 1); + return { doubled: input.n * 2 }; + }), + ); + + const out = yield* stack.deploy( + Effect.gen(function* () { + return yield* Sync({ n: 21 }); + }), + ); + + expect(out).toEqual({ doubled: 42 }); + expect(yield* Ref.get(counter)).toBe(1); + + // Persisted state is `ran` with the materialized output. + const state = yield* yield* State; + const persisted = yield* state.get({ + stack: stack.name, + stage: "test", + fqn: "Sync", + }); + expect(persisted).toMatchObject({ + kind: "action", + status: "ran", + output: { doubled: 42 }, + }); + }), + ); + + test.provider("same input across deploys -> body not re-invoked", (stack) => + Effect.gen(function* () { + const counter = yield* Ref.make(0); + const program = Effect.gen(function* () { + const Sync = Action("Sync", (input: { n: number }) => + Effect.gen(function* () { + yield* Ref.update(counter, (c) => c + 1); + return { doubled: input.n * 2 }; + }), + ); + return yield* Sync({ n: 21 }); + }); + + const first = yield* stack.deploy(program); + const second = yield* stack.deploy(program); + + expect(first).toEqual({ doubled: 42 }); + expect(second).toEqual({ doubled: 42 }); + expect(yield* Ref.get(counter)).toBe(1); + }), + ); + + test.provider("changed input -> body re-invoked", (stack) => + Effect.gen(function* () { + const counter = yield* Ref.make(0); + const programFor = (n: number) => + Effect.gen(function* () { + const Sync = Action("Sync", (input: { n: number }) => + Effect.gen(function* () { + yield* Ref.update(counter, (c) => c + 1); + return { doubled: input.n * 2 }; + }), + ); + return yield* Sync({ n }); + }); + + yield* stack.deploy(programFor(21)); + const second = yield* stack.deploy(programFor(50)); + + expect(second).toEqual({ doubled: 100 }); + expect(yield* Ref.get(counter)).toBe(2); + }), + ); + + test.provider("task output flows to downstream resource input", (stack) => + Effect.gen(function* () { + const Name = Action("Name", (_: {}) => + Effect.succeed({ name: "computed-bucket-name" }), + ); + + const out = yield* stack.deploy( + Effect.gen(function* () { + const computed = yield* Name({}); + return yield* Bucket("MyBucket", { name: computed.name }); + }), + ); + + expect(out.name).toBe("computed-bucket-name"); + }), + ); + + test.provider("resource attr flows to task input", (stack) => + Effect.gen(function* () { + const Echo = Action("Echo", (input: { name: string }) => + Effect.succeed({ echoed: input.name }), + ); + + const out = yield* stack.deploy( + Effect.gen(function* () { + const bucket = yield* Bucket("MyBucket", { name: "from-resource" }); + return yield* Echo({ name: bucket.name }); + }), + ); + + expect(out).toEqual({ echoed: "from-resource" }); + }), + ); + + test.provider( + "removing task from stack drops state without invoking body", + (stack) => + Effect.gen(function* () { + const deleteSpy = yield* Ref.make(0); + const Sync = Action("Sync", (_: { n: number }) => + Effect.succeed({ ok: true }), + ); + + yield* stack.deploy( + Effect.gen(function* () { + return yield* Sync({ n: 1 }); + }), + ); + + const state = yield* yield* State; + expect( + yield* state.get({ stack: stack.name, stage: "test", fqn: "Sync" }), + ).toMatchObject({ kind: "action", status: "ran" }); + + // Re-deploy WITHOUT the task — state should be dropped. + // (Use a tracker hook to confirm body wasn't called.) + yield* stack.deploy(Effect.succeed(undefined)); + void deleteSpy; + + expect( + yield* state.get({ stack: stack.name, stage: "test", fqn: "Sync" }), + ).toBeUndefined(); + }), + ); + + test.provider("init-effect form: deps satisfied at apply", (stack) => + Effect.gen(function* () { + class Multiplier extends Context.Service()( + "test/Multiplier", + ) {} + + const Sync = Action( + "Sync", + Effect.gen(function* () { + const m = yield* Multiplier; + return (input: { n: number }) => + Effect.succeed({ result: input.n * m }); + }), + ); + + const out = yield* stack + .deploy( + Effect.gen(function* () { + return yield* Sync({ n: 21 }); + }), + ) + .pipe(Effect.provideService(Multiplier, 3)); + + expect(out).toEqual({ result: 63 }); + }), + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/apply.test.ts b/.repos/alchemy-effect/packages/alchemy/test/apply.test.ts new file mode 100644 index 00000000000..7a07a38dc0a --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/apply.test.ts @@ -0,0 +1,4430 @@ +import { Cli } from "@/Cli/Cli"; +import * as Construct from "@/Construct"; +import * as Output from "@/Output"; +import { Stack } from "@/Stack"; +import { + type ReplacedResourceState, + type ReplacingResourceState, + type ResourceState, + State, +} from "@/State"; +import * as Test from "@/Test/Vitest"; +import { describe, expect } from "@effect/vitest"; +import { Data, Layer } from "effect"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import { + ArtifactProbe, + BindingTarget, + DeletedBindingRegressionTarget, + DurationResource, + Function, + PhasedTarget, + StaticStablesResource, + TestLayers, + TestResource, + TestResourceHooks, + type TestResourceProps, +} from "./test.resources.ts"; + +const { test } = Test.make({ providers: TestLayers() }); + +const getState = Effect.fn(function* (resourceId: string) { + const state = yield* yield* State; + const stk = yield* Stack; + return (yield* state.get({ + stack: stk.name, + stage: stk.stage, + fqn: resourceId, + })) as S; +}); +const listState = Effect.fn(function* () { + const state = yield* yield* State; + const stk = yield* Stack; + return yield* state.list({ stack: stk.name, stage: stk.stage }); +}); + +const expectConvergedStatus = (status: ResourceState["status"] | undefined) => { + expect(["created", "updated"]).toContain(status); +}; + +// Graceful failure handling means downstream resources of a failed upstream +// may have committed an intermediate "creating"/"replacing" status before +// their `waitForDeps` discovered the upstream failure - or may have fully +// converged using a stable previous output of the failed upstream (e.g. a +// replacement whose old generation is still live). This helper tolerates any +// of those outcomes; the corresponding recovery deploy validates terminal +// state. +const expectNotStarted = (state: ResourceState | undefined) => { + expect([undefined, "creating", "replacing", "created", "updated"]).toContain( + state?.status, + ); +}; + +export class ResourceFailure extends Data.TaggedError("ResourceFailure")<{ + message: string; +}> { + constructor() { + super({ message: `Failed to create` }); + } +} + +const hook = + (hooks?: { + create?: (id: string, props: TestResourceProps) => Effect.Effect; + update?: (id: string, props: TestResourceProps) => Effect.Effect; + delete?: (id: string) => Effect.Effect; + read?: (id: string) => Effect.Effect; + }) => + (test: Effect.Effect) => + test.pipe( + Effect.provide( + Layer.succeed( + TestResourceHooks, + hooks ?? { + create: () => Effect.fail(new ResourceFailure()), + update: () => Effect.fail(new ResourceFailure()), + delete: () => Effect.fail(new ResourceFailure()), + read: () => Effect.succeed(undefined), + }, + ), + ), + // @ts-expect-error - catchTag changes the return type + Effect.catchTag("ResourceFailure", () => Effect.succeed(true)), + ) as Effect.Effect; + +// Helper to fail on specific resource IDs +const failOn = ( + resourceId: string, + hook: "create" | "update" | "delete", +): { + create?: (id: string, props: TestResourceProps) => Effect.Effect; + update?: (id: string, props: TestResourceProps) => Effect.Effect; + delete?: (id: string) => Effect.Effect; +} => ({ + [hook]: (id: string) => + id === resourceId + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), +}); + +// Helper to fail on multiple resource IDs for different hooks +const failOnMultiple = ( + failures: Array<{ id: string; hook: "create" | "update" | "delete" }>, +): { + create?: (id: string, props: TestResourceProps) => Effect.Effect; + update?: (id: string, props: TestResourceProps) => Effect.Effect; + delete?: (id: string) => Effect.Effect; +} => { + const createFailures = failures + .filter((f) => f.hook === "create") + .map((f) => f.id); + const updateFailures = failures + .filter((f) => f.hook === "update") + .map((f) => f.id); + const deleteFailures = failures + .filter((f) => f.hook === "delete") + .map((f) => f.id); + + return { + create: (id: string) => + createFailures.includes(id) + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), + update: (id: string) => + updateFailures.includes(id) + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), + delete: (id: string) => + deleteFailures.includes(id) + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), + }; +}; + +describe("basic operations", () => { + test.provider("should create, update, and delete resources", (stack) => + Effect.gen(function* () { + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + }); + return A.string; + }).pipe(stack.deploy), + ).toEqual("test-string"); + + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string-new", + }); + return A.string; + }).pipe(stack.deploy), + ).toEqual("test-string-new"); + + yield* stack.destroy(); + + expect(yield* getState("A")).toBeUndefined(); + expect(yield* listState()).toEqual([]); + }), + ); + + test.provider("should resolve output properties", (stack) => + Effect.gen(function* () { + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + stringArray: ["test-string-array"], + }); + const B = yield* TestResource("B", { + string: A.string, + }); + return B.string; + }).pipe(stack.deploy), + ).toEqual("test-string"); + + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + stringArray: ["test-string-array"], + }); + const B = yield* TestResource("B", { + string: A.string.pipe(Output.map((string) => string.toUpperCase())), + }); + return B.string; + }).pipe(stack.deploy), + ).toEqual("TEST-STRING"); + + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + stringArray: ["test-string-array"], + }); + const B = yield* TestResource("B", { + string: A.string.pipe( + Output.map((string) => string.toUpperCase() + "-NEW"), + ), + }); + return B.string; + }).pipe(stack.deploy), + ).toEqual("TEST-STRING-NEW"); + + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + stringArray: ["test-string-array"], + }); + const B = yield* TestResource("B", { + string: A.string.pipe( + Output.flatMap((string) => + Output.literal(string.toUpperCase() + "-FLAT"), + ), + ), + }); + return B.string; + }).pipe(stack.deploy), + ).toEqual("TEST-STRING-FLAT"); + }), + ); + + test.provider( + "should resolve bindings inside constructs using namespaced resources", + (stack) => + Effect.gen(function* () { + const Site = Construct.fn(function* (_id: string, _props: {}) { + const bucket = yield* BindingTarget("Bucket", { + string: "bucket-value", + }); + const distribution = yield* BindingTarget("Distribution", { + string: "distribution-value", + }); + + yield* bucket.bind("Policy", { + env: { + BUCKET: bucket.string, + DISTRIBUTION: distribution.string, + }, + }); + + return { + bucket, + distribution, + }; + }); + + const output = yield* Site("MarketingSite", {}).pipe(stack.deploy); + + expect(output.bucket.env).toEqual({ + BUCKET: "bucket-value", + DISTRIBUTION: "distribution-value", + }); + expectConvergedStatus( + (yield* getState("MarketingSite/Bucket"))?.status, + ); + expect((yield* getState("MarketingSite/Distribution"))?.status).toEqual( + "created", + ); + }), + ); + + test.provider( + "should exclude deleted bindings before provider updates", + (stack) => + Effect.gen(function* () { + const created = yield* stack.deploy( + Effect.gen(function* () { + const target = yield* DeletedBindingRegressionTarget("A", { + name: "target", + }); + yield* target.bind("TestBinding", { + env: { + FEATURE_FLAG: "on", + }, + }); + return target; + }), + ); + + expect(created.env).toEqual({ + FEATURE_FLAG: "on", + }); + + const updated = yield* stack.deploy( + Effect.gen(function* () { + return yield* DeletedBindingRegressionTarget("A", { + name: "target", + }); + }), + ); + + expect(updated.env).toEqual({}); + expect(yield* getState("A")).toMatchObject({ + bindings: [], + attr: { + env: {}, + }, + }); + }), + ); + + test.provider( + "should update a surviving consumer before deleting a removed dependency", + (stack) => + Effect.gen(function* () { + const created = yield* stack.deploy( + Effect.gen(function* () { + const secret = yield* TestResource("Secret", { + string: "secret-value", + }); + const worker = yield* Function("Worker", { + name: "worker", + env: { + SECRET: secret.string, + }, + }); + return { secret, worker }; + }), + ); + + expect(created.worker.env).toEqual({ + SECRET: "secret-value", + }); + expect((yield* getState("Secret"))?.status).toEqual("created"); + expect((yield* getState("Worker"))?.status).toEqual("created"); + + const updated = yield* stack.deploy( + Effect.gen(function* () { + return yield* Function("Worker", { + name: "worker", + }); + }), + ); + + expect(updated.env).toEqual({}); + expect(yield* getState("Secret")).toBeUndefined(); + expect((yield* getState("Worker"))?.status).toEqual("updated"); + }), + ); + + test.provider( + "should create a resource with a binding that references its own output", + (stack) => + Effect.gen(function* () { + const created = yield* stack.deploy( + Effect.gen(function* () { + const target = yield* DeletedBindingRegressionTarget("A", { + name: "target", + }); + yield* target.bind("SelfBinding", { + env: { + SELF_NAME: target.name, + }, + }); + return target; + }), + ); + + expect(created.env).toEqual({ + SELF_NAME: "target", + }); + }), + { timeout: 10_000 }, + ); +}); + +describe("linear update propagation", () => { + // Regression: in a linear chain (A -> B with no cycle), an update to A + // followed by an update to B must let B see A's *post-update* attr, never + // the stale prior attr. Before the cycle-gating change, A would publish + // its prior attr early and B's update would race against the live value, + // sometimes deploying with stale data (e.g. a Worker reading a Build's + // outdir/hash before the build finished). + test.provider( + "downstream update receives upstream's post-update attr", + (stack) => + Effect.gen(function* () { + yield* stack.deploy( + Effect.gen(function* () { + const A = yield* TestResource("A", { string: "v1" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }), + ); + + const sawByB: string[] = []; + const captureBHooks = { + create: () => Effect.succeed(undefined), + update: (id: string, props: TestResourceProps) => + Effect.sync(() => { + if (id === "B" && typeof props.string === "string") { + sawByB.push(props.string); + } + }), + delete: () => Effect.succeed(undefined), + read: () => Effect.succeed(undefined), + }; + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "v2" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy, hook(captureBHooks)); + + expect(output.A.string).toEqual("v2"); + expect(output.B.string).toEqual("v2"); + // B.update must have observed the fresh upstream value, never the + // stale "v1". A single fresh-only call is the ideal; we accept any + // sequence as long as no stale value leaked through. + expect(sawByB.length).toBeGreaterThan(0); + expect(sawByB.every((v) => v === "v2")).toBe(true); + }), + ); +}); + +describe("circularity via bindings", () => { + const selfBoundStack = (props: { + string: string; + replaceString?: string; + includeD?: boolean; + }) => + Effect.gen(function* () { + const A = yield* BindingTarget("A", { + name: "a", + string: props.string, + replaceString: props.replaceString, + }); + yield* A.bind("SelfBinding", { + env: { + SELF: A.string, + }, + }); + const B = yield* TestResource("B", { string: A.string }); + if (props.includeD) { + const D = yield* TestResource("D", { string: B.string }); + return { A, B, D }; + } + return { A, B }; + }); + + const mutualBindingStack = (props: { + aString: string; + aReplaceString?: string; + bString?: string; + includeD?: boolean; + }) => + Effect.gen(function* () { + const A = yield* BindingTarget("A", { + name: "a", + string: props.aString, + replaceString: props.aReplaceString, + }); + const B = yield* BindingTarget("B", { + name: "b", + string: props.bString ?? "b-value", + }); + yield* A.bind("FromB", { + env: { + PEER: B.string, + }, + }); + yield* B.bind("FromA", { + env: { + PEER: A.string, + }, + }); + if (props.includeD) { + const D = yield* TestResource("D", { + string: Output.interpolate`${A.string}-${B.string}`, + }); + return { A, B, D }; + } + return { A, B }; + }); + + const propAndBindingCycleStack = () => + Effect.gen(function* () { + const A = yield* BindingTarget("A", { + name: "a", + string: "a-value", + }); + const B = yield* TestResource("B", { + string: A.string, + }); + yield* A.bind("FromB", { + env: { + PEER: B.string, + }, + }); + return { A, B }; + }); + + test.provider( + "create succeeds when props use precreate output and bindings use downstream output", + (stack) => + Effect.gen(function* () { + const output = yield* stack.deploy(propAndBindingCycleStack()); + + expect(output.A.env).toEqual({ PEER: "a-value" }); + expect(output.B.string).toEqual("a-value"); + expectConvergedStatus((yield* getState("A"))?.status); + expectConvergedStatus((yield* getState("B"))?.status); + }), + { timeout: 10_000 }, + ); + + describe("self-referential bindings", () => { + test.provider("create succeeds with self binding", (stack) => + Effect.gen(function* () { + const output = yield* stack.deploy( + selfBoundStack({ + string: "a-value", + replaceString: "original", + }), + ); + + expect(output.A.env).toEqual({ SELF: "a-value" }); + expect(output.B.string).toEqual("a-value"); + expectConvergedStatus((yield* getState("A"))?.status); + expectConvergedStatus((yield* getState("B"))?.status); + }), + ); + + test.provider( + "replacing state noop replay recovers and creates downstream resources", + (stack) => + Effect.gen(function* () { + yield* selfBoundStack({ + string: "a-value", + replaceString: "original", + }).pipe(stack.deploy); + + const program = selfBoundStack({ + string: "a-value-replaced", + replaceString: "changed", + includeD: true, + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect( + (yield* getState("A"))?.status, + ).toEqual("replacing"); + expectConvergedStatus((yield* getState("B"))?.status); + expectNotStarted(yield* getState("D")); + + const output = yield* program.pipe(stack.deploy); + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ SELF: "a-value-replaced" }); + expect(output.D!.string).toEqual("a-value-replaced"); + }), + ); + + test.provider( + "replacing state update replay updates replacement and creates downstream resources", + (stack) => + Effect.gen(function* () { + yield* selfBoundStack({ + string: "a-value", + replaceString: "original", + }).pipe(stack.deploy); + + yield* selfBoundStack({ + string: "a-value-replaced", + replaceString: "changed", + includeD: true, + }).pipe(stack.deploy, hook(failOn("A", "create"))); + + expect( + (yield* getState("A"))?.status, + ).toEqual("replacing"); + expectConvergedStatus((yield* getState("B"))?.status); + expectNotStarted(yield* getState("D")); + + const output = yield* selfBoundStack({ + string: "a-value-updated-during-recovery", + replaceString: "changed", + includeD: true, + }).pipe(stack.deploy); + + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ + SELF: "a-value-updated-during-recovery", + }); + expect(output.D!.string).toEqual("a-value-updated-during-recovery"); + }), + ); + + test.provider( + "replaced state noop replay finishes cleanup and creates downstream resources", + (stack) => + Effect.gen(function* () { + yield* selfBoundStack({ + string: "a-value", + replaceString: "original", + }).pipe(stack.deploy); + + const program = selfBoundStack({ + string: "a-value-replaced", + replaceString: "changed", + includeD: true, + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "update"))); + + expect((yield* getState("A"))?.status).toEqual( + "replaced", + ); + expect((yield* getState("B"))?.status).toEqual("updating"); + expectNotStarted(yield* getState("D")); + + const output = yield* program.pipe(stack.deploy); + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ SELF: "a-value-replaced" }); + expect(output.D!.string).toEqual("a-value-replaced"); + }), + ); + + test.provider( + "replaced state update replay updates replacement and downstream resources", + (stack) => + Effect.gen(function* () { + yield* selfBoundStack({ + string: "a-value", + replaceString: "original", + }).pipe(stack.deploy); + + yield* selfBoundStack({ + string: "a-value-replaced", + replaceString: "changed", + includeD: true, + }).pipe(stack.deploy, hook(failOn("B", "update"))); + + expect((yield* getState("A"))?.status).toEqual( + "replaced", + ); + expect((yield* getState("B"))?.status).toEqual("updating"); + expectNotStarted(yield* getState("D")); + + const output = yield* selfBoundStack({ + string: "a-value-updated-after-replace", + replaceString: "changed", + includeD: true, + }).pipe(stack.deploy); + + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ + SELF: "a-value-updated-after-replace", + }); + expect(output.D!.string).toEqual("a-value-updated-after-replace"); + }), + ); + }); + + describe("mutual A <-> B bindings", () => { + test.provider("create succeeds with mutual bindings", (stack) => + Effect.gen(function* () { + const output = yield* stack.deploy( + mutualBindingStack({ + aString: "a-value", + }), + ); + + expect(output.A.env).toEqual({ PEER: "b-value" }); + expect(output.B.env).toEqual({ PEER: "a-value" }); + expectConvergedStatus((yield* getState("A"))?.status); + expectConvergedStatus((yield* getState("B"))?.status); + }), + ); + + test.provider("destroy succeeds with mutual bindings", (stack) => + Effect.gen(function* () { + yield* mutualBindingStack({ + aString: "a-value", + }).pipe(stack.deploy); + + yield* stack.destroy(); + + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + }), + ); + + describe("from replacing state", () => { + test.provider( + "replacing noop recovery creates downstream resources", + (stack) => + Effect.gen(function* () { + yield* mutualBindingStack({ + aString: "a-value", + aReplaceString: "original", + }).pipe(stack.deploy); + + const program = mutualBindingStack({ + aString: "a-value-replaced", + aReplaceString: "changed", + includeD: true, + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect( + (yield* getState("A"))?.status, + ).toEqual("replacing"); + expectConvergedStatus((yield* getState("B"))?.status); + expectNotStarted(yield* getState("D")); + + const output = yield* program.pipe(stack.deploy); + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ PEER: "b-value" }); + expect(output.B.env).toEqual({ PEER: "a-value-replaced" }); + expect(output.D!.string).toEqual("a-value-replaced-b-value"); + }), + ); + + test.provider( + "replacing update recovery creates downstream resources", + (stack) => + Effect.gen(function* () { + yield* mutualBindingStack({ + aString: "a-value", + aReplaceString: "original", + }).pipe(stack.deploy); + + yield* mutualBindingStack({ + aString: "a-value-replaced", + aReplaceString: "changed", + includeD: true, + }).pipe(stack.deploy, hook(failOn("A", "create"))); + + expect( + (yield* getState("A"))?.status, + ).toEqual("replacing"); + expectConvergedStatus((yield* getState("B"))?.status); + expectNotStarted(yield* getState("D")); + + const output = yield* mutualBindingStack({ + aString: "a-value-updated-during-recovery", + aReplaceString: "changed", + includeD: true, + }).pipe(stack.deploy); + + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ PEER: "b-value" }); + expect(output.B.env).toEqual({ + PEER: "a-value-updated-during-recovery", + }); + expect(output.D!.string).toEqual( + "a-value-updated-during-recovery-b-value", + ); + }), + ); + + test.provider( + "replacing replace recovery nests another replacement", + (stack) => + Effect.gen(function* () { + yield* mutualBindingStack({ + aString: "a-value", + aReplaceString: "original", + }).pipe(stack.deploy); + + yield* mutualBindingStack({ + aString: "a-value-replaced", + aReplaceString: "changed", + includeD: true, + }).pipe(stack.deploy, hook(failOn("A", "create"))); + + const output = yield* mutualBindingStack({ + aString: "a-value-another-replacement", + aReplaceString: "another-change", + includeD: true, + }).pipe(stack.deploy); + + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.B.env).toEqual({ + PEER: "a-value-another-replacement", + }); + }), + ); + }); + + describe("from replaced state", () => { + test.provider( + "replaced noop recovery updates downstream then creates downstream resources", + (stack) => + Effect.gen(function* () { + yield* mutualBindingStack({ + aString: "a-value", + aReplaceString: "original", + }).pipe(stack.deploy); + + const program = mutualBindingStack({ + aString: "a-value-replaced", + aReplaceString: "changed", + includeD: true, + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "update"))); + + expect( + (yield* getState("A"))?.status, + ).toEqual("replaced"); + expect((yield* getState("B"))?.status).toEqual("updating"); + expectNotStarted(yield* getState("D")); + + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ PEER: "b-value" }); + expect(output.B.env).toEqual({ PEER: "a-value-replaced" }); + expect(output.D!.string).toEqual("a-value-replaced-b-value"); + }), + ); + + test.provider( + "replaced with update recovery updates replacement and downstream resources", + (stack) => + Effect.gen(function* () { + yield* mutualBindingStack({ + aString: "a-value", + aReplaceString: "original", + }).pipe(stack.deploy); + + yield* mutualBindingStack({ + aString: "a-value-replaced", + aReplaceString: "changed", + includeD: true, + }).pipe(stack.deploy, hook(failOn("B", "update"))); + + expect( + (yield* getState("A"))?.status, + ).toEqual("replaced"); + expect((yield* getState("B"))?.status).toEqual("updating"); + expectNotStarted(yield* getState("D")); + + const output = yield* mutualBindingStack({ + aString: "a-value-updated-after-replace", + aReplaceString: "changed", + includeD: true, + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.A.env).toEqual({ PEER: "b-value" }); + expect(output.B.env).toEqual({ + PEER: "a-value-updated-after-replace", + }); + expect(output.D!.string).toEqual( + "a-value-updated-after-replace-b-value", + ); + }), + ); + + test.provider( + "replaced replace recovery nests another replacement", + (stack) => + Effect.gen(function* () { + yield* mutualBindingStack({ + aString: "a-value", + aReplaceString: "original", + }).pipe(stack.deploy); + + yield* mutualBindingStack({ + aString: "a-value-replaced", + aReplaceString: "changed", + includeD: true, + }).pipe(stack.deploy, hook(failOn("B", "update"))); + + const output = yield* mutualBindingStack({ + aString: "a-value-another-replacement", + aReplaceString: "another-change", + includeD: true, + }).pipe(stack.deploy); + + expectConvergedStatus((yield* getState("A"))?.status); + expect((yield* getState("B"))?.status).toEqual("updated"); + expectConvergedStatus((yield* getState("D"))?.status); + expect(output.B.env).toEqual({ + PEER: "a-value-another-replacement", + }); + }), + ); + }); + }); +}); + +describe("prop-flow convergence", () => { + const phasedCycleStack = (props: { + desired: string; + replaceKey?: string; + use: "stableId" | "value"; + includeC?: boolean; + }) => + Effect.gen(function* () { + const A = yield* PhasedTarget("A", { + desired: props.desired, + replaceKey: props.replaceKey, + }); + const selected = props.use === "stableId" ? A.stableId : A.value; + const B = yield* TestResource("B", { + string: selected, + }); + yield* A.bind("FromB", { + env: { + B: B.string, + }, + }); + + if (props.includeC) { + const C = yield* TestResource("C", { + string: B.string, + }); + return { A, B, C }; + } + + return { A, B }; + }); + + test.provider( + "fresh circular create may use a stable precreate identifier", + (stack) => + Effect.gen(function* () { + const output = yield* phasedCycleStack({ + desired: "final-a", + replaceKey: "v1", + use: "stableId", + }).pipe(stack.deploy); + + expect(output.A.value).toEqual("final-a"); + expect(output.B.string).toEqual("stable:v1"); + }), + ); + + test.provider( + "fresh circular create should converge downstream props to final values", + (stack) => + Effect.gen(function* () { + const output = yield* phasedCycleStack({ + desired: "final-a", + replaceKey: "v1", + use: "value", + }).pipe(stack.deploy); + + expect(output.A.value).toEqual("final-a"); + expect(output.B.string).toEqual("final-a"); + }), + ); + + test.provider( + "fresh replacement should converge newly created downstream props to replacement values", + (stack) => + Effect.gen(function* () { + yield* phasedCycleStack({ + desired: "old-a", + replaceKey: "v1", + use: "value", + }).pipe(stack.deploy); + + const output = yield* phasedCycleStack({ + desired: "new-a", + replaceKey: "v2", + use: "value", + }).pipe(stack.deploy); + + expect(output.A.value).toEqual("new-a"); + expect(output.B.string).toEqual("new-a"); + }), + ); + + test.provider( + "stale precreate values should not propagate transitively", + (stack) => + Effect.gen(function* () { + const output = yield* phasedCycleStack({ + desired: "final-a", + replaceKey: "v1", + use: "value", + includeC: true, + }).pipe(stack.deploy); + + expect(output.A.value).toEqual("final-a"); + expect(output.B.string).toEqual("final-a"); + expect(output.C!.string).toEqual("final-a"); + }), + ); + + test.provider( + "binding feedback converges across an A -> B -> A fixed point", + (stack) => + Effect.gen(function* () { + const output = yield* Effect.gen(function* () { + const A = yield* PhasedTarget("A", { + desired: "final-a", + replaceKey: "v1", + }); + const B = yield* TestResource("B", { + string: A.value, + }); + yield* A.bind("FromB", { + env: { + B: B.string, + }, + }); + return { A, B }; + }).pipe(stack.deploy); + + expect(output.A.value).toEqual("final-a"); + expect(output.B.string).toEqual("final-a"); + expect(output.A.env).toEqual({ + B: "final-a", + }); + }), + ); + + test.provider( + "terminal created or updated status is delayed until fixed-point convergence finishes", + (stack) => + Effect.gen(function* () { + const events: Array<{ id: string; status: string }> = []; + const cli = Cli.of({ + approvePlan: () => Effect.succeed(true), + displayPlan: () => Effect.void, + startApplySession: () => + Effect.succeed({ + done: () => Effect.void, + emit: (event) => + Effect.sync(() => { + if (event.kind === "status-change") { + events.push({ + id: event.id, + status: event.status, + }); + } + }), + }), + }); + + const output = yield* Effect.gen(function* () { + const A = yield* PhasedTarget("A", { + desired: "final-a", + replaceKey: "v1", + }); + const B = yield* TestResource("B", { + string: A.value, + }); + yield* A.bind("FromB", { + env: { + B: B.string, + }, + }); + return { A, B }; + }).pipe(stack.deploy, Effect.provide(Layer.succeed(Cli, cli))); + + expect(output.A.env).toEqual({ + B: "final-a", + }); + + const statusesById = events.reduce( + (acc, event: { id: string; status: string }) => { + (acc[event.id] ??= []).push(event); + return acc; + }, + {} as Record>, + ); + const terminal = (id: string) => + (statusesById[id] ?? []) + .map((event: { id: string; status: string }) => event.status) + .filter( + (status: string) => status === "created" || status === "updated", + ); + + expect(terminal("A")).toEqual(["updated"]); + expect(terminal("B")).toEqual(["updated"]); + }), + ); + + // Regression: a resource with `precreate` (e.g. Cloudflare Worker) resolves + // its early `ready` signal before its real `reconcile` runs. A non-cyclic + // downstream must still wait for the upstream's TERMINAL output, so that an + // upstream `reconcile` failure interrupts the downstream instead of letting + // it proceed off the precreate stub. Before the fix the downstream raced + // ahead on the precreate identifier and fully created itself even though the + // upstream failed. + test.provider( + "precreate upstream reconcile failure interrupts non-cyclic downstream (stable id dep)", + (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* PhasedTarget("A", { + desired: "a-value", + replaceKey: "v1", + }); + // B depends on A.stableId — a value already available from A's + // precreate stub — yet must still be gated on A's reconcile. + const B = yield* TestResource("B", { + string: A.stableId, + }); + return { A, B }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + // A's reconcile failed after committing "creating". + expect((yield* getState("A"))?.status).toEqual("creating"); + // B must NOT have reached its own reconcile. It may have committed an + // intermediate "creating" while waiting on deps, but it must never be + // "created" — that would mean the upstream failure was ignored. + expect((yield* getState("B"))?.status).not.toEqual("created"); + + // Recovery deploy converges both. + const output = yield* program.pipe(stack.deploy); + expectConvergedStatus((yield* getState("A"))?.status); + expectConvergedStatus((yield* getState("B"))?.status); + expect(output.B.string).toEqual("stable:v1"); + }), + ); + + test.provider( + "precreate upstream reconcile failure interrupts non-cyclic downstream (value dep)", + (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* PhasedTarget("A", { + desired: "a-value", + replaceKey: "v1", + }); + const B = yield* TestResource("B", { + string: A.value, + }); + const C = yield* TestResource("C", { + string: B.string, + }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect((yield* getState("A"))?.status).toEqual("creating"); + expect((yield* getState("B"))?.status).not.toEqual("created"); + // Transitive downstream never starts either. + expectNotStarted(yield* getState("C")); + + const output = yield* program.pipe(stack.deploy); + expectConvergedStatus((yield* getState("A"))?.status); + expectConvergedStatus((yield* getState("B"))?.status); + expectConvergedStatus((yield* getState("C"))?.status); + expect(output.C.string).toEqual("a-value"); + }), + ); +}); + +describe("from created state", () => { + test.provider("noop when props unchanged", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + }); + return A.string; + }); + + let output = yield* stack.deploy(program); + expect(output).toEqual("test-string"); + + expect((yield* getState("A"))?.status).toEqual("created"); + output = yield* stack.deploy(program); + + // Re-apply with same props - should be noop + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("test-string"); + }), + ); + + test.provider("replace when props trigger replacement", (stack) => + Effect.gen(function* () { + yield* stack.deploy( + Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "original", + }); + return A.replaceString; + }), + ); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Change props that trigger replacement + + const output = yield* stack.deploy( + Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "new", + }); + return A.replaceString; + }), + ); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("new"); + }), + ); +}); + +describe("from updated state", () => { + test.provider("noop when props unchanged", (stack) => + Effect.gen(function* () { + yield* stack.deploy( + Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }), + ); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Update to get to updated state + yield* stack.deploy( + Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string-changed", + }); + }), + ); + expect((yield* getState("A"))?.status).toEqual("updated"); + + // Re-apply with same props - should be noop + const output = yield* stack.deploy( + Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string-changed", + }); + return A.string; + }), + ); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect(output).toEqual("test-string-changed"); + }), + ); + + test.provider("replace when props trigger replacement", (stack) => + Effect.gen(function* () { + yield* stack.deploy( + Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + replaceString: "original", + }); + }), + ); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Update to get to updated state + yield* stack.deploy( + Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string-changed", + replaceString: "original", + }); + }), + ); + expect((yield* getState("A"))?.status).toEqual("updated"); + + // Change props that trigger replacement + const output = yield* stack.deploy( + Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string-changed", + replaceString: "new", + }); + return A.replaceString; + }), + ); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("new"); + }), + ); +}); + +describe("from creating state", () => { + test.provider("continue creating when props unchanged", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }).pipe(stack.deploy, hook()); + expect((yield* getState("A"))?.status).toEqual("creating"); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + }); + return A.string; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("test-string"); + }), + ); + + test.provider( + "continue creating when props have updatable changes", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }).pipe(stack.deploy, hook()); + expect((yield* getState("A"))?.status).toEqual("creating"); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string-changed", + }); + return A.string; + }).pipe(stack.deploy); + expect(output).toEqual("test-string-changed"); + expect((yield* getState("A"))?.status).toEqual("created"); + }), + ); + + test.provider("replace when props trigger replacement", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "test-string", + }); + }).pipe(stack.deploy, hook()); + expect((yield* getState("A"))?.status).toEqual("creating"); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "test-string-changed", + }); + return A.replaceString; + }).pipe(stack.deploy); + expect(output).toEqual("test-string-changed"); + expect((yield* getState("A"))?.status).toEqual("created"); + }), + ); + + test.provider( + "destroy should handle creating state with no attributes", + (stack) => + Effect.gen(function* () { + // 1. Create a resource but fail - this leaves state in "creating" with no attr + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }).pipe(stack.deploy, hook()); + expect((yield* getState("A"))?.status).toEqual("creating"); + expect((yield* getState("A"))?.attr).toBeUndefined(); + + // 2. Call destroy - this triggers collectGarbage which tries to delete + // the orphaned resource. The bug is that output is undefined in the + // delete call when the resource never completed creation. + yield* stack.destroy(); + + // Resource should be cleaned up + expect(yield* getState("A")).toBeUndefined(); + }), + ); + + test.provider( + "destroy should handle creating state when attributes can be recovered", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }).pipe(stack.deploy, hook()); + expect((yield* getState("A"))?.status).toEqual("creating"); + expect((yield* getState("A"))?.attr).toBeUndefined(); + + yield* stack.destroy().pipe( + hook({ + delete: () => Effect.fail(new ResourceFailure()), + read: () => + Effect.succeed({ + string: "test-string", + }), + }), + ); + + // Resource should be cleaned up + expect((yield* getState("A"))?.status).toEqual("deleting"); + + // actually delete this time + yield* stack.destroy().pipe( + hook({ + read: () => + Effect.succeed({ + string: "test-string", + }), + }), + ); + + expect(yield* getState("A")).toBeUndefined(); + }), + ); + + test.provider( + "destroy should handle replacing state when old resource has no attributes", + (stack) => + Effect.gen(function* () { + // 1. Create a resource but fail - this leaves state in "creating" with no attr + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "original", + }); + }).pipe(stack.deploy, hook()); + expect((yield* getState("A"))?.status).toEqual("creating"); + expect((yield* getState("A"))?.attr).toBeUndefined(); + + // 2. Trigger replacement but also fail during create - this leaves state in "replacing" + // with old.attr being undefined + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "new", + }); + }).pipe(stack.deploy, hook()); + const state = yield* getState("A"); + expect(state?.status).toEqual("replacing"); + expect(state?.old?.attr).toBeUndefined(); + + // 3. Call destroy - this triggers collectGarbage which tries to delete + // the resource. The bug is that old.attr is undefined. + yield* stack.destroy().pipe( + hook({ + read: () => + Effect.succeed({ + replaceString: "original", + }), + }), + ); + + // Resource should be cleaned up + expect(yield* getState("A")).toBeUndefined(); + }), + ); +}); + +describe("from updating state", () => { + test.provider("continue updating when props unchanged", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string-changed", + }); + }).pipe( + stack.deploy, + hook({ + update: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("updating"); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string-changed", + }); + return A.string; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect(output).toEqual("test-string-changed"); + }), + ); + + test.provider( + "continue updating when props have updatable changes", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string-changed", + }); + }).pipe( + stack.deploy, + hook({ + update: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("updating"); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string-changed-again", + }); + return A.string; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect(output).toEqual("test-string-changed-again"); + }), + ); + + test.provider("replace when props trigger replacement", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + replaceString: "original", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string-changed", + replaceString: "original", + }); + }).pipe( + stack.deploy, + hook({ + update: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("updating"); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string-changed", + replaceString: "changed", + }); + return A.replaceString; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("changed"); + }), + ); +}); + +describe("from replacing state", () => { + test.provider("continue replacement when props unchanged", (stack) => + Effect.gen(function* () { + // 1. Create initial resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "original", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + // 2. Trigger replacement but fail during create of replacement + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "new", + }); + }).pipe( + stack.deploy, + hook({ + create: () => Effect.fail(new ResourceFailure()), + }), + ); + const state = yield* getState("A"); + expect(state?.status).toEqual("replacing"); + expect(state?.old?.status).toEqual("created"); + + // 3. Re-apply with same props - should continue replacement + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "new", + }); + return A.replaceString; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("new"); + }), + ); + + test.provider( + "continue replacement when props have updatable changes", + (stack) => + Effect.gen(function* () { + // 1. Create initial resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "original", + string: "initial", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + // 2. Trigger replacement but fail during create + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "new", + string: "initial", + }); + }).pipe( + stack.deploy, + hook({ + create: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("replacing"); + + // 3. Re-apply with changed props (updatable) - should continue replacement with new props + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "new", + string: "changed", + }); + return { replaceString: A.replaceString, string: A.string }; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output.replaceString).toEqual("new"); + expect(output.string).toEqual("changed"); + }), + ); + + test.provider( + "continue replacement when props trigger another replacement", + (stack) => + Effect.gen(function* () { + // 1. Create initial resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "original", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + // 2. Trigger replacement but fail during create + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "new", + }); + }).pipe( + stack.deploy, + hook({ + create: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("replacing"); + + // 3. Replace again with another replacement - should converge + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "another-replacement", + }); + return A.replaceString; + }).pipe(stack.deploy); + expectConvergedStatus((yield* getState("A"))?.status); + expect(output).toEqual("another-replacement"); + }), + ); +}); + +describe("from replaced state", () => { + test.provider("continue cleanup when props unchanged", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "test-string", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "test-string-changed", + }); + }).pipe( + stack.deploy, + hook({ + delete: () => Effect.fail(new ResourceFailure()), + }), + ); + const AState = yield* getState("A"); + expect(AState?.status).toEqual("replaced"); + expect(AState?.old).toMatchObject({ + status: "created", + props: { + replaceString: "test-string", + }, + }); + + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "test-string-changed", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + }), + ); + + test.provider( + "update replacement then cleanup when props have updatable changes", + (stack) => + Effect.gen(function* () { + // 1. Create initial resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "original", + string: "initial", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + // 2. Trigger replacement and fail during delete of old resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "new", + string: "initial", + }); + }).pipe( + stack.deploy, + hook({ + delete: () => Effect.fail(new ResourceFailure()), + }), + ); + const state = yield* getState("A"); + expect(state?.status).toEqual("replaced"); + expect(state?.old?.status).toEqual("created"); + + // 3. Change props again (updatable change) - should update the replacement then cleanup + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "new", + string: "changed", + }); + return { replaceString: A.replaceString, string: A.string }; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output.replaceString).toEqual("new"); + expect(output.string).toEqual("changed"); + }), + ); + + test.provider( + "continue cleanup when props trigger another replacement", + (stack) => + Effect.gen(function* () { + // 1. Create initial resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "original", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + // 2. Trigger replacement and fail during delete of old resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "new", + }); + }).pipe( + stack.deploy, + hook({ + delete: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("replaced"); + + // 3. Replace again and continue cleanup of the older generations + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "another-replacement", + }); + return A.replaceString; + }).pipe(stack.deploy); + expectConvergedStatus((yield* getState("A"))?.status); + expect(output).toEqual("another-replacement"); + }), + ); +}); + +describe("from deleting state", () => { + test.provider( + "create when props unchanged or have updatable changes", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "test-string", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + yield* stack.destroy().pipe( + hook({ + delete: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("deleting"); + + // Now re-apply with the same props - should create the resource again + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + }); + return A.string; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("test-string"); + }), + ); + + test.provider("create when props trigger replacement", (stack) => + Effect.gen(function* () { + // 1. Create initial resource + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "original", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + // 2. Try to delete but fail + yield* stack.destroy().pipe( + hook({ + delete: () => Effect.fail(new ResourceFailure()), + }), + ); + expect((yield* getState("A"))?.status).toEqual("deleting"); + + // 3. Re-apply with props that trigger replacement - should recreate + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + replaceString: "new", + }); + return A.replaceString; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect(output).toEqual("new"); + }), + ); +}); + +// ============================================================================= +// DEPENDENT RESOURCES (A -> B where B depends on A.string) +// ============================================================================= + +describe("dependent resources (A -> B)", () => { + describe("happy path", () => { + test.provider("create A then B where B uses A.string", (stack) => + Effect.gen(function* () { + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.string).toEqual("a-value"); + expect(output.B.string).toEqual("a-value"); + }), + ); + + test.provider("update A propagates to B", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Update A's string - B should update with the new value + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value-updated" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.A.string).toEqual("a-value-updated"); + expect(output.B.string).toEqual("a-value-updated"); + }), + ); + + test.provider("replace A, B updates to new A's output", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Replace A - B should update to point to new A's output + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.A.string).toEqual("a-value-new"); + expect(output.B.string).toEqual("a-value-new"); + }), + ); + + test.provider("delete both resources (B deleted first, then A)", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + + yield* stack.destroy(); + + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + expect(yield* listState()).toEqual([]); + }), + ); + }); + + describe("failures during expandAndPivot", () => { + test.provider( + "A create fails, B never starts - recovery creates both", + (stack) => + Effect.gen(function* () { + // A fails to create - B should never start + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy, hook(failOn("A", "create"))); + + expect((yield* getState("A"))?.status).toEqual("creating"); + expectNotStarted(yield* getState("B")); + + // Recovery: re-apply should create both + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.string).toEqual("a-value"); + expect(output.B.string).toEqual("a-value"); + }), + ); + + test.provider("A creates, B create fails - recovery creates B", (stack) => + Effect.gen(function* () { + // A succeeds, B fails to create + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy, hook(failOn("B", "create"))); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("creating"); + + // Recovery: re-apply should noop A and create B + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.B.string).toEqual("a-value"); + }), + ); + + test.provider("A update fails - recovery updates both", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value-updated" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }); + + // A fails to update - B should not start updating + yield* program.pipe(stack.deploy, hook(failOn("A", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updating"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Recovery: re-apply should update both + const output = yield* program.pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.A.string).toEqual("a-value-updated"); + expect(output.B.string).toEqual("a-value-updated"); + }), + ); + + test.provider("A updates, B update fails - recovery updates B", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value-updated" }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }); + + // A succeeds, B fails to update + yield* program.pipe(stack.deploy, hook(failOn("B", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updating"); + + // Recovery: re-apply should noop A and update B + const output = yield* program.pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.B.string).toEqual("a-value-updated"); + }), + ); + + test.provider( + "A replacement fails - recovery replaces A and updates B", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }); + + // A replacement fails (during create of new A) - B should not start + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect( + (yield* getState("A"))?.status, + ).toEqual("replacing"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Recovery: re-apply should complete A replacement and update B + const output = yield* program.pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.A.string).toEqual("a-value-new"); + expect(output.B.string).toEqual("a-value-new"); + }), + ); + + test.provider( + "A replaced, B update fails - recovery updates B then cleans up", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }); + + // A replacement succeeds, B fails to update + yield* program.pipe(stack.deploy, hook(failOn("B", "update"))); + + // A should be in replaced state (new A created, old A pending cleanup) + // B should be in updating state + const aState = yield* getState("A"); + expect(aState?.status).toEqual("replaced"); + expect((yield* getState("B"))?.status).toEqual("updating"); + + // Recovery: re-apply should update B and clean up old A + const output = yield* program.pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.B.string).toEqual("a-value-new"); + }), + ); + }); + + describe("failures during collectGarbage", () => { + test.provider( + "A replaced, B updated, old A delete fails - recovery cleans up", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }); + + // A replacement and B update succeed, but old A delete fails + yield* program.pipe(stack.deploy, hook(failOn("A", "delete"))); + + // A should be in replaced state (delete of old A failed) + // B should have been updated successfully + expect((yield* getState("A"))?.status).toEqual( + "replaced", + ); + expect((yield* getState("B"))?.status).toEqual("updated"); + + // Recovery: re-apply should clean up old A + const output = yield* program.pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.A.string).toEqual("a-value-new"); + }), + ); + + test.provider( + "orphan B delete fails - recovery deletes B then A", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Orphan deletion: B delete fails + yield* stack.destroy().pipe(hook(failOn("B", "delete"))); + + // B should be in deleting state, A should still be created (waiting for B) + expect((yield* getState("B"))?.status).toEqual("deleting"); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Recovery: re-apply destroy should delete B then A + yield* stack.destroy(); + + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + }), + ); + + test.provider( + "orphan A delete fails after B deleted - recovery deletes A", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Orphan deletion: B succeeds, A fails + yield* stack.destroy().pipe(hook(failOn("A", "delete"))); + + // B should be deleted, A should be in deleting state + expectNotStarted(yield* getState("B")); + expect((yield* getState("A"))?.status).toEqual("deleting"); + + // Recovery: re-apply destroy should delete A + yield* stack.destroy(); + + expect(yield* getState("A")).toBeUndefined(); + }), + ); + }); +}); + +// ============================================================================= +// THREE-LEVEL DEPENDENCY CHAIN (A -> B -> C where C depends on B, B depends on A) +// ============================================================================= + +describe("three-level dependency chain (A -> B -> C)", () => { + describe("happy path", () => { + test.provider("create A then B then C", (stack) => + Effect.gen(function* () { + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect(output.A.string).toEqual("a-value"); + expect(output.B.string).toEqual("a-value"); + expect(output.C.string).toEqual("a-value"); + }), + ); + + test.provider("update A propagates through B to C", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value-updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-updated"); + }), + ); + + test.provider("replace A propagates through B to C", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-new"); + }), + ); + + test.provider("delete all three (C first, then B, then A)", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + yield* stack.destroy(); + + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + expect(yield* listState()).toEqual([]); + }), + ); + }); + + describe("creation failures", () => { + test.provider("A create fails - B and C never start", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect((yield* getState("A"))?.status).toEqual("creating"); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect(output.C.string).toEqual("a-value"); + }), + ); + + test.provider("A creates, B create fails - C never starts", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "create"))); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("creating"); + expectNotStarted(yield* getState("C")); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect(output.C.string).toEqual("a-value"); + }), + ); + + test.provider("A and B create, C create fails", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("C", "create"))); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("creating"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect(output.C.string).toEqual("a-value"); + }), + ); + }); + + describe("update failures", () => { + test.provider("A update fails - B and C remain stable", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value-updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updating"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-updated"); + }), + ); + + test.provider("A updates, B update fails - C remains stable", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value-updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updating"); + expect((yield* getState("C"))?.status).toEqual("created"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-updated"); + }), + ); + + test.provider("A and B update, C update fails", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value-updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("C", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updating"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-updated"); + }), + ); + }); + + describe("replace cascade failures", () => { + test.provider("A replace fails - B and C remain stable", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect((yield* getState("A"))?.status).toEqual( + "replacing", + ); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-new"); + }), + ); + + test.provider("A replaced, B update fails - C remains stable", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "update"))); + + expect((yield* getState("A"))?.status).toEqual( + "replaced", + ); + expect((yield* getState("B"))?.status).toEqual("updating"); + expect((yield* getState("C"))?.status).toEqual("created"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-new"); + }), + ); + + test.provider("A replaced, B updated, C update fails", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("C", "update"))); + + expect((yield* getState("A"))?.status).toEqual( + "replaced", + ); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updating"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-new"); + }), + ); + + test.provider( + "A replaced, B and C updated, old A delete fails - recovery cleans up", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "delete"))); + + expect((yield* getState("A"))?.status).toEqual( + "replaced", + ); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-value-new"); + }), + ); + }); + + describe("delete order failures", () => { + test.provider("C delete fails - A and B waiting", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + yield* stack.destroy().pipe(hook(failOn("C", "delete"))); + + expect((yield* getState("C"))?.status).toEqual("deleting"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Recovery + yield* stack.destroy(); + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + }), + ); + + test.provider("C deleted, B delete fails - A waiting", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + yield* stack.destroy().pipe(hook(failOn("B", "delete"))); + + expectNotStarted(yield* getState("C")); + expect((yield* getState("B"))?.status).toEqual("deleting"); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Recovery + yield* stack.destroy(); + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + }), + ); + + test.provider("C and B deleted, A delete fails", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + yield* stack.destroy().pipe(hook(failOn("A", "delete"))); + + expectNotStarted(yield* getState("C")); + expectNotStarted(yield* getState("B")); + expect((yield* getState("A"))?.status).toEqual("deleting"); + + // Recovery + yield* stack.destroy(); + expect(yield* getState("A")).toBeUndefined(); + }), + ); + }); +}); + +// ============================================================================= +// DIAMOND DEPENDENCIES (D depends on B and C, both depend on A) +// A +// / \ +// B C +// \ / +// D +// ============================================================================= + +describe("diamond dependencies (A -> B,C -> D)", () => { + describe("happy path", () => { + test.provider("create all four resources", (stack) => + Effect.gen(function* () { + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.D.string).toEqual("a-value-a-value"); + }), + ); + + test.provider("update A propagates to B, C, and D", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + }).pipe(stack.deploy); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect((yield* getState("D"))?.status).toEqual("updated"); + expect(output.D.string).toEqual("updated-updated"); + }), + ); + + test.provider("add D while B replaces and C noops", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { + string: Output.interpolate`${A.string}-b`, + replaceString: "b-original", + }); + yield* TestResource("C", { + string: Output.interpolate`${A.string}-c`, + replaceString: "c-original", + }); + }).pipe(stack.deploy); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { + string: Output.interpolate`${A.string}-b-replaced`, + replaceString: "b-changed", + }); + const C = yield* TestResource("C", { + string: Output.interpolate`${A.string}-c`, + replaceString: "c-original", + }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.B.replaceString).toEqual("b-changed"); + expect(output.C.replaceString).toEqual("c-original"); + expect(output.D.string).toEqual("a-value-b-replaced-a-value-c"); + }), + ); + + test.provider("add D while C replaces and B noops", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { + string: Output.interpolate`${A.string}-b`, + replaceString: "b-original", + }); + yield* TestResource("C", { + string: Output.interpolate`${A.string}-c`, + replaceString: "c-original", + }); + }).pipe(stack.deploy); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { + string: Output.interpolate`${A.string}-b`, + replaceString: "b-original", + }); + const C = yield* TestResource("C", { + string: Output.interpolate`${A.string}-c-replaced`, + replaceString: "c-changed", + }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.B.replaceString).toEqual("b-original"); + expect(output.C.replaceString).toEqual("c-changed"); + expect(output.D.string).toEqual("a-value-b-a-value-c-replaced"); + }), + ); + + test.provider("add D while both B and C replace", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { + string: Output.interpolate`${A.string}-b`, + replaceString: "b-original", + }); + yield* TestResource("C", { + string: Output.interpolate`${A.string}-c`, + replaceString: "c-original", + }); + }).pipe(stack.deploy); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { + string: Output.interpolate`${A.string}-b-replaced`, + replaceString: "b-changed", + }); + const C = yield* TestResource("C", { + string: Output.interpolate`${A.string}-c-replaced`, + replaceString: "c-changed", + }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.B.replaceString).toEqual("b-changed"); + expect(output.C.replaceString).toEqual("c-changed"); + expect(output.D.string).toEqual( + "a-value-b-replaced-a-value-c-replaced", + ); + }), + ); + + test.provider("delete all (D first, then B and C, then A)", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + }).pipe(stack.deploy); + + yield* stack.destroy(); + + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + expectNotStarted(yield* getState("D")); + }), + ); + }); + + describe("creation failures", () => { + test.provider("A create fails - B, C, D never start", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect((yield* getState("A"))?.status).toEqual("creating"); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + expectNotStarted(yield* getState("D")); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.D.string).toEqual("a-value-a-value"); + }), + ); + + test.provider( + "A creates, B create fails - C may create, D stuck", + (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "create"))); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("creating"); + // C might have been created since it doesn't depend on B + const cState = yield* getState("C"); + expect(cState === undefined || cState?.status === "created").toBe( + true, + ); + expectNotStarted(yield* getState("D")); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.D.string).toEqual("a-value-a-value"); + }), + ); + + test.provider( + "A creates, C create fails - B may create, D stuck", + (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("C", "create"))); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("creating"); + // B might have been created since it doesn't depend on C + const bState = yield* getState("B"); + expect(bState === undefined || bState?.status === "created").toBe( + true, + ); + expectNotStarted(yield* getState("D")); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.D.string).toEqual("a-value-a-value"); + }), + ); + + test.provider("A, B, C create - D create fails", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("D", "create"))); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("creating"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.D.string).toEqual("a-value-a-value"); + }), + ); + + test.provider("both B and C fail to create - D stuck", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe( + stack.deploy, + hook( + failOnMultiple([ + { id: "B", hook: "create" }, + { id: "C", hook: "create" }, + ]), + ), + ); + + expect((yield* getState("A"))?.status).toEqual("created"); + // effect terminates eagerly, so it's possible that B or C to run first and block C from running + const BState = yield* getState("B"); + const CState = yield* getState("C"); + expect(BState?.status).toBeOneOf(["creating", undefined]); + expect(CState?.status).toBeOneOf(["creating", undefined]); + // at leasst one of B or C should have been created + expect(BState?.status ?? CState?.status).toEqual("creating"); + + expectNotStarted(yield* getState("D")); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + expect(output.D.string).toEqual("a-value-a-value"); + }), + ); + }); + + describe("update failures", () => { + test.provider("A update fails - B, C, D remain stable", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updating"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect((yield* getState("D"))?.status).toEqual("updated"); + expect(output.D.string).toEqual("updated-updated"); + }), + ); + + test.provider( + "A updates, B update fails - C may update, D stuck", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updating"); + // C might have been updated since it doesn't depend on B + const cState = yield* getState("C"); + expect( + cState?.status === "created" || cState?.status === "updated", + ).toBe(true); + expect((yield* getState("D"))?.status).toEqual("created"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect((yield* getState("D"))?.status).toEqual("updated"); + expect(output.D.string).toEqual("updated-updated"); + }), + ); + + test.provider("A, B, C update - D update fails", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "updated" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + return { A, B, C, D }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("D", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect((yield* getState("D"))?.status).toEqual("updating"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("D"))?.status).toEqual("updated"); + expect(output.D.string).toEqual("updated-updated"); + }), + ); + }); + + describe("delete failures", () => { + test.provider("D delete fails - B, C, A waiting", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + }).pipe(stack.deploy); + + yield* stack.destroy().pipe(hook(failOn("D", "delete"))); + + expect((yield* getState("D"))?.status).toEqual("deleting"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Recovery + yield* stack.destroy(); + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + expectNotStarted(yield* getState("D")); + }), + ); + + test.provider( + "D deleted, B delete fails - C may delete, A waiting", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: A.string }); + yield* TestResource("D", { + string: Output.interpolate`${B.string}-${C.string}`, + }); + }).pipe(stack.deploy); + + yield* stack.destroy().pipe(hook(failOn("B", "delete"))); + + expectNotStarted(yield* getState("D")); + expect((yield* getState("B"))?.status).toEqual("deleting"); + // C may or may not be deleted depending on execution order + const cState = yield* getState("C"); + expect(cState === undefined || cState?.status === "created").toBe( + true, + ); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Recovery + yield* stack.destroy(); + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + }), + ); + }); +}); + +// ============================================================================= +// INDEPENDENT RESOURCES (no dependencies between them) +// ============================================================================= + +describe("independent resources (A, B with no dependencies)", () => { + describe("parallel failures", () => { + test.provider("both A and B fail to create", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: "b-value" }); + return { A, B }; + }); + + yield* program.pipe( + stack.deploy, + hook( + failOnMultiple([ + { id: "A", hook: "create" }, + { id: "B", hook: "create" }, + ]), + ), + ); + + // effect terminates eagerly, so it's possible that A or B runs first and blocks the other from running + const AState = yield* getState("A"); + const BState = yield* getState("B"); + expect(AState?.status).toBeOneOf(["creating", undefined]); + expect(BState?.status).toBeOneOf(["creating", undefined]); + // at least one of A or B should have been creating + expect(AState?.status ?? BState?.status).toEqual("creating"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.string).toEqual("a-value"); + expect(output.B.string).toEqual("b-value"); + }), + ); + + test.provider("A creates, B fails - recovery creates B", (stack) => + Effect.gen(function* () { + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: "b-value" }); + return { A, B }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("B", "create"))); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("creating"); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.B.string).toEqual("b-value"); + }), + ); + + test.provider("A update fails, B update succeeds", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: "b-value" }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-updated" }); + const B = yield* TestResource("B", { string: "b-updated" }); + return { A, B }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "update"))); + + expect((yield* getState("A"))?.status).toEqual("updating"); + // B might have been updated + const bState = yield* getState("B"); + expect( + bState?.status === "created" || bState?.status === "updated", + ).toBe(true); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.A.string).toEqual("a-updated"); + expect(output.B.string).toEqual("b-updated"); + }), + ); + }); + + describe("mixed state recovery", () => { + test.provider( + "A in creating, B in updating state - recovery completes both", + (stack) => + Effect.gen(function* () { + // First create B successfully + yield* Effect.gen(function* () { + yield* TestResource("B", { string: "b-value" }); + }).pipe(stack.deploy); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Now try to create A and update B - A fails + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: "b-updated" }); + return { A, B }; + }); + + yield* program.pipe( + stack.deploy, + hook( + failOnMultiple([ + { id: "A", hook: "create" }, + { id: "B", hook: "update" }, + ]), + ), + ); + + // effect terminates eagerly, so it's possible that A or B runs first and blocks the other from running + const AState = yield* getState("A"); + const BState = yield* getState("B"); + expect(AState?.status).toBeOneOf(["creating", undefined]); + expect(BState?.status).toBeOneOf(["created", "updating"]); + // at least one of A or B should have started their failing operation + expect( + AState?.status === "creating" || BState?.status === "updating", + ).toBe(true); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect(output.A.string).toEqual("a-value"); + expect(output.B.string).toEqual("b-updated"); + }), + ); + + test.provider( + "A in replacing, B in deleting state - complex recovery", + (stack) => + Effect.gen(function* () { + // Create both + yield* Effect.gen(function* () { + yield* TestResource("A", { replaceString: "original" }); + yield* TestResource("B", { string: "b-value" }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Try to replace A and delete B (by not including B) - both fail + const program = Effect.gen(function* () { + yield* TestResource("A", { replaceString: "changed" }); + }); + + yield* program.pipe( + stack.deploy, + hook( + failOnMultiple([ + { id: "A", hook: "create" }, + { id: "B", hook: "delete" }, + ]), + ), + ); + + // effect terminates eagerly, so it's possible that A or B runs first and blocks the other from running + const AState = yield* getState("A"); + const BState = yield* getState("B"); + expect(AState?.status).toBeOneOf(["created", "replacing"]); + expect(BState?.status).toBeOneOf(["created", "deleting"]); + // at least one of A or B should have started their failing operation + expect( + AState?.status === "replacing" || BState?.status === "deleting", + ).toBe(true); + + // Recovery - complete the replace and delete + yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expectNotStarted(yield* getState("B")); + }), + ); + }); +}); + +// ============================================================================= +// MULTIPLE RESOURCES REPLACING SIMULTANEOUSLY +// ============================================================================= + +describe("multiple resources replacing", () => { + test.provider("two independent resources replace successfully", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-original" }); + yield* TestResource("B", { replaceString: "b-original" }); + }).pipe(stack.deploy); + + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { replaceString: "a-new" }); + const B = yield* TestResource("B", { replaceString: "b-new" }); + return { A, B }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.replaceString).toEqual("a-new"); + expect(output.B.replaceString).toEqual("b-new"); + }), + ); + + test.provider( + "A replace fails, B replace succeeds - recovery completes A", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-original" }); + yield* TestResource("B", { replaceString: "b-original" }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { replaceString: "a-new" }); + const B = yield* TestResource("B", { replaceString: "b-new" }); + return { A, B }; + }); + + yield* program.pipe(stack.deploy, hook(failOn("A", "create"))); + + expect((yield* getState("A"))?.status).toEqual( + "replacing", + ); + // B might have been replaced + const bState = yield* getState("B"); + expect( + bState?.status === "created" || + bState?.status === "replacing" || + bState?.status === "replaced", + ).toBe(true); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.replaceString).toEqual("a-new"); + expect(output.B.replaceString).toEqual("b-new"); + }), + ); + + test.provider( + "both A and B replace fail - recovery completes both", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-original" }); + yield* TestResource("B", { replaceString: "b-original" }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { replaceString: "a-new" }); + const B = yield* TestResource("B", { replaceString: "b-new" }); + return { A, B }; + }); + + yield* program.pipe( + stack.deploy, + hook( + failOnMultiple([ + { id: "A", hook: "create" }, + { id: "B", hook: "create" }, + ]), + ), + ); + + // effect terminates eagerly, so it's possible that A or B runs first and blocks the other from running + const AState = yield* getState("A"); + const BState = yield* getState("B"); + expect(AState?.status).toBeOneOf(["created", "replacing"]); + expect(BState?.status).toBeOneOf(["created", "replacing"]); + // at least one of A or B should have started replacing + expect( + AState?.status === "replacing" || BState?.status === "replacing", + ).toBe(true); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.replaceString).toEqual("a-new"); + expect(output.B.replaceString).toEqual("b-new"); + }), + ); + + test.provider( + "A replaced, B replacing - old A delete fails, B create fails - recovery completes both", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-original" }); + yield* TestResource("B", { replaceString: "b-original" }); + }).pipe(stack.deploy); + + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { replaceString: "a-new" }); + const B = yield* TestResource("B", { replaceString: "b-new" }); + return { A, B }; + }); + + yield* program.pipe( + stack.deploy, + hook( + failOnMultiple([ + { id: "A", hook: "delete" }, + { id: "B", hook: "create" }, + ]), + ), + ); + + // effect terminates eagerly, so it's possible that A or B runs first and blocks the other from running + // A should be replaced (new created, old pending delete) or still replacing/created if B failed first + // B should be replacing (new not yet created) or already created if A failed first + const AState = yield* getState("A"); + const BState = yield* getState("B"); + expect(AState?.status).toBeOneOf(["created", "replacing", "replaced"]); + expect(BState?.status).toBeOneOf(["created", "replacing"]); + // at least one of A or B should have started their failing operation + expect( + AState?.status === "replaced" || BState?.status === "replacing", + ).toBe(true); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.replaceString).toEqual("a-new"); + expect(output.B.replaceString).toEqual("b-new"); + }), + ); +}); + +describe("repeated replacements", () => { + test.provider( + "resource can be replaced again while still in replacing state", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-original" }); + }).pipe(stack.deploy); + + const firstReplacement = Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-first" }); + }); + + yield* firstReplacement.pipe(stack.deploy, hook(failOn("A", "create"))); + + const replacingState = yield* getState("A"); + expect(replacingState?.status).toEqual("replacing"); + + const secondReplacement = Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-second" }); + }); + + yield* secondReplacement.pipe(stack.deploy); + + const finalState = yield* getState("A"); + expectConvergedStatus(finalState?.status); + expect(finalState?.props?.replaceString).toEqual("a-second"); + }), + ); + + test.provider( + "resource can be replaced again while still in replaced state", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-original" }); + }).pipe(stack.deploy); + + const firstReplacement = Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-first" }); + }); + + yield* firstReplacement.pipe(stack.deploy, hook(failOn("A", "delete"))); + + const replacedState = yield* getState("A"); + expect(replacedState?.status).toEqual("replaced"); + + const secondReplacement = Effect.gen(function* () { + yield* TestResource("A", { replaceString: "a-second" }); + }); + + yield* secondReplacement.pipe(stack.deploy); + + const finalState = yield* getState("A"); + expectConvergedStatus(finalState?.status); + expect(finalState?.props?.replaceString).toEqual("a-second"); + }), + ); +}); + +// ============================================================================= +// ORPHAN CHAIN DELETION +// ============================================================================= + +describe("orphan chain deletion", () => { + test.provider("three-level orphan chain deleted in correct order", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + + // Remove C from graph - should delete C only + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expectNotStarted(yield* getState("C")); + }), + ); + + test.provider( + "orphan with intermediate failure recovers correctly", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + // Remove all three - C fails to delete + yield* stack.destroy().pipe(hook(failOn("C", "delete"))); + + expect((yield* getState("C"))?.status).toEqual("deleting"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Recovery + yield* stack.destroy(); + expect(yield* getState("A")).toBeUndefined(); + expectNotStarted(yield* getState("B")); + expectNotStarted(yield* getState("C")); + }), + ); + + test.provider("partial orphan - remove leaf, add new dependent", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: A.string }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + + // Remove B, add C dependent on A + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const C = yield* TestResource("C", { string: A.string }); + return { A, C }; + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expectNotStarted(yield* getState("B")); + expect((yield* getState("C"))?.status).toEqual("created"); + expect(output.C.string).toEqual("a-value"); + }), + ); +}); + +// ============================================================================= +// COMPLEX MIXED STATE SCENARIOS +// ============================================================================= + +describe("complex mixed state scenarios", () => { + test.provider("replace upstream while creating downstream", (stack) => + Effect.gen(function* () { + // Create A + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + + // Now add B dependent on A, and also replace A + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value-new", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect(output.A.string).toEqual("a-value-new"); + expect(output.B.string).toEqual("a-value-new"); + }), + ); + + test.provider("update upstream, create and delete in same apply", (stack) => + Effect.gen(function* () { + // Create A and B + yield* Effect.gen(function* () { + yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { string: "b-value" }); + }).pipe(stack.deploy); + + // Update A, delete B (by not including), create C + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-updated" }); + const C = yield* TestResource("C", { string: A.string }); + return { A, C }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("updated"); + expectNotStarted(yield* getState("B")); + expect((yield* getState("C"))?.status).toEqual("created"); + expect(output.C.string).toEqual("a-updated"); + }), + ); + + test.provider( + "chain reaction: A replace triggers B update triggers C update", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + const B = yield* TestResource("B", { string: A.string }); + yield* TestResource("C", { string: B.string }); + }).pipe(stack.deploy); + + // Replace A - should cascade updates to B and C + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-replaced", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.string }); + const C = yield* TestResource("C", { string: B.string }); + return { A, B, C }; + }).pipe(stack.deploy); + + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("updated"); + expect(output.C.string).toEqual("a-replaced"); + }), + ); + + test.provider("multiple failures across all operation types", (stack) => + Effect.gen(function* () { + // Setup: A, B created; C, D will be added + yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "a-value", + replaceString: "original", + }); + yield* TestResource("B", { string: "b-value" }); + }).pipe(stack.deploy); + + // Complex operation: A replace, B update, C create, D not included (nothing to delete) + const program = Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "a-replaced", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: "b-updated" }); + const C = yield* TestResource("C", { string: "c-value" }); + return { A, B, C }; + }); + + // Fail on A replace (create phase) and C create + yield* program.pipe( + stack.deploy, + hook( + failOnMultiple([ + { id: "A", hook: "create" }, + { id: "C", hook: "create" }, + ]), + ), + ); + + // effect terminates eagerly, so it's possible that A or C runs first and blocks the other from running + const AState = yield* getState("A"); + // B might have been updated + const bState = yield* getState("B"); + expect(bState?.status === "created" || bState?.status === "updated").toBe( + true, + ); + const CState = yield* getState("C"); + expect(AState?.status).toBeOneOf(["created", "replacing"]); + expect(CState?.status).toBeOneOf(["creating", undefined]); + // at least one of A or C should have started their failing operation + expect( + AState?.status === "replacing" || CState?.status === "creating", + ).toBe(true); + + // Recovery + const output = yield* program.pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect(output.A.replaceString).toEqual("changed"); + expect(output.B.string).toEqual("b-updated"); + expect(output.C.string).toEqual("c-value"); + }), + ); +}); + +describe("artifacts", () => { + test.provider("shares artifacts from plan diff into apply update", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* ArtifactProbe("A", { value: "v1" }); + }).pipe(stack.deploy); + + const updated = yield* Effect.gen(function* () { + const A = yield* ArtifactProbe("A", { value: "v2" }); + return { A }; + }).pipe(stack.deploy); + + expect(updated.A.value).toEqual("v2"); + expect(updated.A.artifactValue).toEqual("v2"); + expect((yield* getState("A"))?.status).toEqual("updated"); + }), + ); + + test.provider( + "isolates artifact bags by FQN for namespaced resources with the same leaf logical ID", + (stack) => + Effect.gen(function* () { + const Site = Construct.fn(function* ( + _id: string, + props: { value: string }, + ) { + return yield* ArtifactProbe("Shared", { value: props.value }); + }); + + yield* Effect.gen(function* () { + yield* Site("Left", { value: "left-v1" }); + yield* Site("Right", { value: "right-v1" }); + }).pipe(stack.deploy); + + const updated = yield* Effect.gen(function* () { + const left = yield* Site("Left", { value: "left-v2" }); + const right = yield* Site("Right", { value: "right-v2" }); + return { left, right }; + }).pipe(stack.deploy); + + expect(updated.left.artifactValue).toEqual("left-v2"); + expect(updated.right.artifactValue).toEqual("right-v2"); + expect((yield* getState("Left/Shared"))?.status).toEqual("updated"); + expect((yield* getState("Right/Shared"))?.status).toEqual("updated"); + }), + ); +}); + +// ============================================================================= +// STATIC STABLE PROPERTIES (provider.stables defined on provider, not in diff) +// This tests the bug where diff returns undefined but downstream resources +// depend on stable properties that should be preserved +// ============================================================================= + +describe("static stable properties (provider.stables)", () => { + describe("diff returns undefined with tag-only changes", () => { + test.provider( + "upstream has static stables, diff returns undefined, downstream depends on stableId", + (stack) => + Effect.gen(function* () { + // Stage 1: Create A with no tags, B depends on A.stableId + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { string: "value" }); + const B = yield* TestResource("B", { string: A.stableId }); + return { A, B }; + }).pipe(stack.deploy); + expect(output.A.stableId).toEqual("stable-A"); + expect(output.B.string).toEqual("stable-A"); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("created"); + } + + // Stage 2: Add tags to A - diff returns undefined, but arePropsChanged is true + // B depends on A.stableId which should remain stable + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "value", + tags: { Name: "tagged-resource" }, + }); + const B = yield* TestResource("B", { string: A.stableId }); + return { A, B }; + }).pipe(stack.deploy); + // A should be updated (tags changed) + expect(output.A.tags).toEqual({ Name: "tagged-resource" }); + // B should NOT be updated because stableId didn't change + expect(output.B.string).toEqual("stable-A"); + expect((yield* getState("A"))?.status).toEqual("updated"); + // B should remain "created" (noop) since its input (stableId) didn't change + expect((yield* getState("B"))?.status).toEqual("created"); + } + }), + ); + + test.provider( + "chain: A -> B -> C where B depends on A.stableId and C depends on B.stableString", + (stack) => + Effect.gen(function* () { + // Stage 1: Create chain + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "initial", + }); + const B = yield* TestResource("B", { string: A.stableId }); + const C = yield* TestResource("C", { string: B.stableString }); + return { A, B, C }; + }).pipe(stack.deploy); + expect(output.A.stableId).toEqual("stable-A"); + expect(output.B.string).toEqual("stable-A"); + expect(output.C.string).toEqual("B"); + } + + // Stage 2: Change A's tags only - diff returns undefined + // Neither B nor C should update since their inputs are stable + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "initial", + tags: { Env: "production" }, + }); + const B = yield* TestResource("B", { string: A.stableId }); + const C = yield* TestResource("C", { string: B.stableString }); + return { A, B, C }; + }).pipe(stack.deploy); + expect(output.A.tags).toEqual({ Env: "production" }); + expect((yield* getState("A"))?.status).toEqual("updated"); + // B and C should not change + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + } + }), + ); + + test.provider( + "diamond: A -> B,C -> D where all depend on stable properties", + (stack) => + Effect.gen(function* () { + // Stage 1: Create diamond + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "initial", + }); + const B = yield* TestResource("B", { string: A.stableId }); + const C = yield* TestResource("C", { string: A.stableArn }); + const D = yield* TestResource("D", { + string: Output.interpolate`${B.stableString}-${C.stableString}`, + }); + return { A, B, C, D }; + }).pipe(stack.deploy); + expect(output.A.stableId).toEqual("stable-A"); + expect(output.A.stableArn).toEqual( + "arn:test:resource:us-east-1:123456789:A", + ); + expect(output.B.string).toEqual("stable-A"); + expect(output.C.string).toEqual( + "arn:test:resource:us-east-1:123456789:A", + ); + expect(output.D.string).toEqual("B-C"); + } + + // Stage 2: Change A's tags - should not affect B, C, or D + { + yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "initial", + tags: { Team: "platform" }, + }); + const B = yield* TestResource("B", { string: A.stableId }); + const C = yield* TestResource("C", { string: A.stableArn }); + yield* TestResource("D", { + string: Output.interpolate`${B.stableString}-${C.stableString}`, + }); + }).pipe(stack.deploy); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("created"); + expect((yield* getState("C"))?.status).toEqual("created"); + expect((yield* getState("D"))?.status).toEqual("created"); + } + }), + ); + }); + + describe("diff returns update action with static stables", () => { + test.provider( + "upstream has static stables and diff returns update, downstream depends on stableId", + (stack) => + Effect.gen(function* () { + // Stage 1: Create A and B + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "value-1", + }); + const B = yield* TestResource("B", { string: A.stableId }); + return { A, B }; + }).pipe(stack.deploy); + expect(output.A.stableId).toEqual("stable-A"); + expect(output.B.string).toEqual("stable-A"); + } + + // Stage 2: Change A's string - diff returns "update", stableId still stable + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "value-2", + }); + const B = yield* TestResource("B", { string: A.stableId }); + return { A, B }; + }).pipe(stack.deploy); + expect(output.A.string).toEqual("value-2"); + expect(output.A.stableId).toEqual("stable-A"); + expect((yield* getState("A"))?.status).toEqual("updated"); + // B should not change since stableId is stable + expect((yield* getState("B"))?.status).toEqual("created"); + } + }), + ); + + test.provider( + "downstream depends on non-stable property, should update", + (stack) => + Effect.gen(function* () { + // Stage 1: Create A and B where B depends on A.string (non-stable) + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "value-1", + }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + expect(output.A.string).toEqual("value-1"); + expect(output.B.string).toEqual("value-1"); + } + + // Stage 2: Change A's string - B should update + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "value-2", + }); + const B = yield* TestResource("B", { string: A.string }); + return { A, B }; + }).pipe(stack.deploy); + expect(output.A.string).toEqual("value-2"); + expect(output.B.string).toEqual("value-2"); + expect((yield* getState("A"))?.status).toEqual("updated"); + expect((yield* getState("B"))?.status).toEqual("updated"); + } + }), + ); + }); + + describe("replace action with static stables", () => { + test.provider( + "upstream replaces, downstream depends on stableId - should update with new value", + (stack) => + Effect.gen(function* () { + // Stage 1: Create A and B + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "value", + replaceString: "original", + }); + const B = yield* TestResource("B", { string: A.stableId }); + return { A, B }; + }).pipe(stack.deploy); + expect(output.A.stableId).toEqual("stable-A"); + expect(output.B.string).toEqual("stable-A"); + } + + // Stage 2: Replace A - stableId will change (new resource) + { + const output = yield* Effect.gen(function* () { + const A = yield* StaticStablesResource("A", { + string: "value", + replaceString: "changed", + }); + const B = yield* TestResource("B", { string: A.stableId }); + return { A, B }; + }).pipe(stack.deploy); + // A was replaced, stableId is regenerated + expect(output.A.stableId).toEqual("stable-A"); + expect(output.B.string).toEqual("stable-A"); + expect((yield* getState("A"))?.status).toEqual("created"); + expect((yield* getState("B"))?.status).toEqual("updated"); + } + }), + ); + }); +}); + +describe("Redacted props/outputs survive deploy", () => { + test.provider( + "preserves a Redacted prop end-to-end through create", + (stack) => + Effect.gen(function* () { + const secret = Redacted.make("hunter2"); + const created = yield* Effect.gen(function* () { + return yield* TestResource("A", { + string: "x", + redacted: secret, + }); + }).pipe(stack.deploy); + + expect(Redacted.isRedacted(created.redacted)).toBe(true); + expect(Redacted.value(created.redacted!)).toBe("hunter2"); + + const state = yield* getState("A"); + expect(state).toBeDefined(); + expect(Redacted.isRedacted((state!.props as any).redacted)).toBe(true); + expect(Redacted.value((state!.props as any).redacted)).toBe("hunter2"); + expect(Redacted.isRedacted((state!.attr as any).redacted)).toBe(true); + expect(Redacted.value((state!.attr as any).redacted)).toBe("hunter2"); + }), + ); + + test.provider( + "preserves Redacted values nested inside an array end-to-end", + (stack) => + Effect.gen(function* () { + const created = yield* Effect.gen(function* () { + return yield* TestResource("A", { + string: "x", + redactedArray: [Redacted.make("a"), Redacted.make("b")], + }); + }).pipe(stack.deploy); + + expect(created.redactedArray).toBeDefined(); + expect(created.redactedArray!.length).toBe(2); + expect(Redacted.isRedacted(created.redactedArray![0]!)).toBe(true); + expect(Redacted.value(created.redactedArray![0]!)).toBe("a"); + expect(Redacted.isRedacted(created.redactedArray![1]!)).toBe(true); + expect(Redacted.value(created.redactedArray![1]!)).toBe("b"); + }), + ); + + test.provider( + "preserves a Redacted output flowing into a downstream resource prop", + (stack) => + Effect.gen(function* () { + const output = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "x", + redacted: Redacted.make("hunter2"), + }); + const B = yield* TestResource("B", { + string: "y", + redacted: A.redacted as any, + }); + return { A, B }; + }).pipe(stack.deploy); + + expect(Redacted.isRedacted(output.B.redacted)).toBe(true); + expect(Redacted.value(output.B.redacted!)).toBe("hunter2"); + + const bState = yield* getState("B"); + expect(Redacted.isRedacted((bState!.props as any).redacted)).toBe(true); + expect(Redacted.value((bState!.props as any).redacted)).toBe("hunter2"); + expect(Redacted.isRedacted((bState!.attr as any).redacted)).toBe(true); + expect(Redacted.value((bState!.attr as any).redacted)).toBe("hunter2"); + }), + ); + + test.provider( + "no-op redeploy when only Redacted prop is present and value unchanged", + (stack) => + Effect.gen(function* () { + const first = yield* Effect.gen(function* () { + return yield* TestResource("A", { + string: "x", + redacted: Redacted.make("hunter2"), + }); + }).pipe(stack.deploy); + expect(Redacted.value(first.redacted!)).toBe("hunter2"); + + const before = yield* getState("A"); + + yield* Effect.gen(function* () { + return yield* TestResource("A", { + string: "x", + redacted: Redacted.make("hunter2"), + }); + }).pipe(stack.deploy); + + const after = yield* getState("A"); + expect(after?.status).toBe("created"); + expect((before as any).updatedAt ?? null).toEqual( + (after as any).updatedAt ?? null, + ); + expect(Redacted.value((after!.attr as any).redacted)).toBe("hunter2"); + }), + ); + + test.provider("update redeploy when Redacted prop value changes", (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + return yield* TestResource("A", { + string: "x", + redacted: Redacted.make("old"), + }); + }).pipe(stack.deploy); + + const updated = yield* Effect.gen(function* () { + return yield* TestResource("A", { + string: "x", + redacted: Redacted.make("new"), + }); + }).pipe(stack.deploy); + + expect(Redacted.isRedacted(updated.redacted)).toBe(true); + expect(Redacted.value(updated.redacted!)).toBe("new"); + const state = yield* getState("A"); + expect(state?.status).toBe("updated"); + expect(Redacted.value((state!.attr as any).redacted)).toBe("new"); + }), + ); +}); + +describe("stack output persistence", () => { + const getStackOutput = (stack: string, stage: string) => + Effect.gen(function* () { + const state = yield* yield* State; + return yield* state.getOutput({ stack, stage }); + }); + + test.provider( + "apply persists the resolved stack output via state.setOutput", + (stack) => + Effect.gen(function* () { + const result = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "hello" }); + return { url: A.string }; + }).pipe(stack.deploy); + expect(result).toEqual({ url: "hello" }); + + const persisted = yield* getStackOutput(stack.name, "test").pipe( + Effect.provide(stack.state), + ); + expect(persisted).toEqual({ url: "hello" }); + }), + ); + + test.provider( + "redeploys overwrite the persisted stack output with the new value", + (stack) => + Effect.gen(function* () { + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "v1" }); + return { url: A.string }; + }).pipe(stack.deploy); + + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "v2" }); + return { url: A.string }; + }).pipe(stack.deploy); + + const persisted = yield* getStackOutput(stack.name, "test").pipe( + Effect.provide(stack.state), + ); + expect(persisted).toEqual({ url: "v2" }); + }), + ); + + test.provider( + "another stack can read the persisted output via Output.stackRef", + (stack) => + Effect.gen(function* () { + // First deploy: write the stack output we'll later reference. + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "shared" }); + return { url: A.string }; + }).pipe(stack.deploy); + + // Second deploy: a downstream resource consumes the previously + // persisted stack output via Output.stackRef. The deploy + // succeeds because state.getOutput finds it. + const result = yield* Effect.gen(function* () { + const upstream = yield* Output.stackRef<{ url: string }>(stack.name); + const B = yield* TestResource("B", { + string: (upstream as any).url, + }); + return { downstream: B.string }; + }).pipe(stack.deploy); + + expect(result).toEqual({ downstream: "shared" }); + }), + ); +}); + +describe("Duration round-trip through state", () => { + test.provider( + "input Duration reaches reconcile as a real Duration and output Duration re-hydrates as a real Duration on the next deploy", + (stack) => + Effect.gen(function* () { + const first = yield* stack.deploy( + DurationResource("Timer", { timeout: Duration.seconds(15) }), + ); + + // Reconcile saw a real Duration: arithmetic worked. + expect(Duration.isDuration(first.observedTimeout)).toBe(true); + expect(Duration.toMillis(first.observedTimeout)).toBe(15_000); + expect(Duration.isDuration(first.computedTimeout)).toBe(true); + expect(Duration.toMillis(first.computedTimeout)).toBe(16_000); + + // Second deploy: identical props. The engine reads the previous + // output from state. If the Duration weren't revived, `output` + // (a plain `{_id,_tag,millis}` shape) would fail `isDuration` and + // `Duration.toMillis` would throw. + const second = yield* stack.deploy( + DurationResource("Timer", { timeout: Duration.seconds(15) }), + ); + expect(Duration.isDuration(second.observedTimeout)).toBe(true); + expect(Duration.toMillis(second.observedTimeout)).toBe(15_000); + expect(Duration.isDuration(second.computedTimeout)).toBe(true); + expect(Duration.toMillis(second.computedTimeout)).toBe(16_000); + + // The persisted state itself should round-trip to a real Duration. + const persisted = yield* getState<{ + attr: DurationResource["Attributes"]; + }>("Timer"); + expect(Duration.isDuration(persisted.attr.computedTimeout)).toBe(true); + expect(Duration.toMillis(persisted.attr.computedTimeout)).toBe(16_000); + }), + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/plan.test.ts b/.repos/alchemy-effect/packages/alchemy/test/plan.test.ts new file mode 100644 index 00000000000..67d434dde15 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/plan.test.ts @@ -0,0 +1,2881 @@ +import { AdoptPolicy, Unowned } from "@/AdoptPolicy"; +import * as Construct from "@/Construct"; +import { dedupeBindings } from "@/Diff"; +import type { Input, InputProps } from "@/Input"; +import * as Output from "@/Output"; +import * as Plan from "@/Plan"; +import { UnsatisfiedResourceCycle } from "@/Plan"; +import type { ResourceBinding } from "@/Resource"; +import * as Stack from "@/Stack"; +import { Stage } from "@/Stage"; +import { + InMemoryService, + inMemoryState, + State, + type ResourceState, + type ResourceStatus, +} from "@/State"; +import * as Test from "@/Test/Vitest"; +import { describe, expect } from "@effect/vitest"; +import * as Cause from "effect/Cause"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Redacted from "effect/Redacted"; +import { + ArtifactProbe, + BindingTarget, + Bucket, + Function, + NoPrecreateBindingTarget, + Queue, + TestLayers, + TestResource, + TestResourceHooks, + type TestResourceProps, +} from "./test.resources"; + +const TEST_STACK = "test"; +const TEST_STAGE = "test"; + +// Fresh in-memory state per test run so seeded resources from one test +// don't leak into another in the same file. +const freshState = Layer.effect( + State, + Effect.sync(() => InMemoryService({})), +); + +const { test } = Test.make({ + providers: TestLayers(), + state: freshState, +}); + +// Resolve stack name/stage from ambient Stack if present (for test.provider) +// otherwise fall back to the file-level defaults (for plain test()). +const resolveStackId = Effect.gen(function* () { + const ambient = yield* Effect.serviceOption(Stack.Stack); + return Option.match(ambient, { + onNone: () => ({ name: TEST_STACK, stage: TEST_STAGE }), + onSome: (s) => ({ name: s.name, stage: s.stage }), + }); +}); + +const seed = (resources: Record) => + Effect.gen(function* () { + const { name, stage } = yield* resolveStackId; + const state = yield* yield* State; + for (const [fqn, value] of Object.entries(resources)) { + yield* state.set({ stack: name, stage, fqn, value }); + } + }); + +const instanceId = "852f6ec2e19b66589825efe14dca2971"; + +const makePlan = ( + effect: Effect.Effect, + options?: Plan.MakePlanOptions, +): Effect.Effect, Err, State> => + // @ts-expect-error - Stack.make's typing erases R unsoundly here + Effect.gen(function* () { + const { name, stage } = yield* resolveStackId; + // @ts-expect-error + return yield* effect.pipe( + // @ts-expect-error + Stack.make({ + name, + providers: Layer.empty, + state: inMemoryState(), + }), + Effect.provideService(Stage, stage), + Effect.flatMap((stackSpec: any) => Plan.make(stackSpec, options)), + Effect.provide(TestLayers()), + ); + }); + +const makePlanWithCustomStack = + (stackSpec: any) => + ( + effect: Effect.Effect, + ): Effect.Effect, Err, State> => + // @ts-expect-error + Effect.gen(function* () { + const { name, stage } = yield* resolveStackId; + // @ts-expect-error + return yield* effect.pipe( + // @ts-expect-error + Stack.make({ + name, + providers: Layer.empty, + state: inMemoryState(), + stack: stackSpec, + }), + Effect.provideService(Stage, stage), + Effect.flatMap(Plan.make), + Effect.provide(TestLayers()), + ); + }); + +test( + "artifacts are isolated by FQN during plan diff for namespaced resources", + Effect.gen(function* () { + yield* seed({ + "Left/Shared": { + instanceId: "left-instance", + providerVersion: 0, + logicalId: "Shared", + fqn: "Left/Shared", + namespace: { Id: "Left" }, + resourceType: "Test.ArtifactProbe", + status: "created", + props: { + value: "left-v1", + }, + attr: { + value: "left-v1", + artifactValue: undefined, + }, + bindings: [], + downstream: [], + }, + "Right/Shared": { + instanceId: "right-instance", + providerVersion: 0, + logicalId: "Shared", + fqn: "Right/Shared", + namespace: { Id: "Right" }, + resourceType: "Test.ArtifactProbe", + status: "created", + props: { + value: "right-v1", + }, + attr: { + value: "right-v1", + artifactValue: undefined, + }, + bindings: [], + downstream: [], + }, + }); + const Site = Construct.fn(function* ( + _id: string, + props: { value: string }, + ) { + return yield* ArtifactProbe("Shared", { value: props.value }); + }); + + const plan = yield* Effect.gen(function* () { + const left = yield* Site("Left", { value: "left-v2" }); + const right = yield* Site("Right", { value: "right-v2" }); + return { left, right }; + }).pipe(makePlan); + + expect(plan.resources["Left/Shared"]?.action).toEqual("update"); + expect(plan.resources["Right/Shared"]?.action).toEqual("update"); + }), +); + +test( + "create all resources when plan is empty", + Effect.gen(function* () { + expect( + yield* Effect.gen(function* () { + const bucket = yield* Bucket("MyBucket", { + name: "test-bucket", + }); + const queue = yield* Queue("MyQueue", { + name: "test-queue", + }); + + return { + queueUrl: queue.queueUrl, + bucketArn: bucket.bucketArn, + }; + }).pipe(makePlan), + ).toMatchObject({ + resources: { + MyBucket: { + action: "create", + bindings: [], + props: { + name: "test-bucket", + }, + state: undefined, + }, + MyQueue: { + action: "create", + bindings: [], + props: { + name: "test-queue", + }, + state: undefined, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test( + "update the changed resources and no-op un-changed resources", + Effect.gen(function* () { + yield* seed({ + MyBucket: { + instanceId, + providerVersion: 0, + logicalId: "MyBucket", + fqn: "MyBucket", + namespace: undefined, + resourceType: "Test.Bucket", + status: "created", + props: { + name: "test-bucket", + }, + attr: { + name: "test-bucket", + }, + bindings: [], + downstream: [], + }, + }); + expect( + yield* makePlan( + Effect.gen(function* () { + yield* Bucket("MyBucket", { + name: "test-bucket", + }); + yield* Queue("MyQueue", { + name: "test-queue", + }); + }), + ), + ).toMatchObject({ + resources: { + MyBucket: { + action: "noop", + bindings: [], + state: { + status: "created", + }, + }, + MyQueue: { + action: "create", + bindings: [], + props: { + name: "test-queue", + }, + state: undefined, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test( + "force changes noop resources into updates", + Effect.gen(function* () { + yield* seed({ + MyBucket: { + instanceId, + providerVersion: 0, + logicalId: "MyBucket", + fqn: "MyBucket", + namespace: undefined, + resourceType: "Test.Bucket", + status: "created", + props: { + name: "test-bucket", + }, + attr: { + name: "test-bucket", + }, + bindings: [], + downstream: [], + }, + }); + expect( + yield* makePlan( + Effect.gen(function* () { + yield* Bucket("MyBucket", { + name: "test-bucket", + }); + }), + { force: true }, + ), + ).toMatchObject({ + resources: { + MyBucket: { + action: "update", + bindings: [], + props: { + name: "test-bucket", + }, + state: { + status: "created", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test( + "no-op resources with undefined props", + Effect.gen(function* () { + yield* seed({ + MyQueue: { + instanceId, + providerVersion: 0, + logicalId: "MyQueue", + fqn: "MyQueue", + namespace: undefined, + resourceType: "Test.Queue", + status: "created", + props: undefined as any, + attr: { + name: "MyQueue", + queueUrl: "https://test.queue.com/MyQueue", + }, + bindings: [], + downstream: [], + }, + }); + expect( + yield* makePlan( + Effect.gen(function* () { + yield* Queue("MyQueue"); + }), + ), + ).toMatchObject({ + resources: { + MyQueue: { + action: "noop", + bindings: [], + state: { + status: "created", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test( + "no-op resources when object prop key order changes", + Effect.gen(function* () { + yield* seed({ + MyFunction: { + instanceId, + providerVersion: 0, + logicalId: "MyFunction", + fqn: "MyFunction", + namespace: undefined, + resourceType: "Test.Function", + status: "created", + props: { + name: "test-function", + env: { + A: "1", + B: "2", + }, + }, + attr: { + name: "test-function", + env: { + A: "1", + B: "2", + }, + functionArn: "arn:test:function:MyFunction", + }, + bindings: [], + downstream: [], + }, + }); + expect( + yield* makePlan( + Effect.gen(function* () { + yield* Function("MyFunction", { + name: "test-function", + env: { + B: "2", + A: "1", + }, + }); + }), + ), + ).toMatchObject({ + resources: { + MyFunction: { + action: "noop", + bindings: [], + state: { + status: "created", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test( + "delete orphaned resources", + Effect.gen(function* () { + yield* seed({ + MyBucket: { + instanceId, + providerVersion: 0, + logicalId: "MyBucket", + fqn: "MyBucket", + namespace: undefined, + resourceType: "Test.Bucket", + status: "created", + props: { + name: "test-bucket", + }, + attr: { + name: "test-bucket", + }, + bindings: [], + downstream: [], + }, + MyQueue: { + instanceId, + providerVersion: 0, + logicalId: "MyQueue", + fqn: "MyQueue", + namespace: undefined, + resourceType: "Test.Queue", + status: "created", + props: { + name: "test-queue", + }, + attr: { + name: "test-queue", + }, + bindings: [], + downstream: [], + }, + }); + expect( + yield* makePlan( + Effect.gen(function* () { + yield* Queue("MyQueue", { + name: "test-queue", + }); + }), + ), + ).toMatchObject({ + resources: { + MyQueue: { + action: "noop", + bindings: [], + state: { + status: "created", + }, + }, + }, + deletions: { + MyBucket: { + action: "delete", + bindings: [], + state: { + status: "created", + attr: { + name: "test-bucket", + }, + }, + resource: { + LogicalId: "MyBucket", + Type: "Test.Bucket", + Props: { + name: "test-bucket", + }, + }, + }, + }, + }); + }), +); + +test( + "allow deleting a resource after a surviving consumer removes the dependency", + Effect.gen(function* () { + yield* seed({ + Secret: { + instanceId, + providerVersion: 0, + logicalId: "Secret", + fqn: "Secret", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "secret-value", + }, + attr: { + string: "secret-value", + stringArray: [], + stableString: "Secret", + stableArray: ["Secret"], + replaceString: undefined, + }, + bindings: [], + downstream: ["Worker"], + }, + Worker: { + instanceId, + providerVersion: 0, + logicalId: "Worker", + fqn: "Worker", + namespace: undefined, + resourceType: "Test.Function", + status: "created", + props: { + name: "worker", + env: { + SECRET: "secret-value", + }, + }, + attr: { + name: "worker", + env: { + SECRET: "secret-value", + }, + functionArn: "arn:aws:lambda:us-west-2:084828582823:function:Worker", + }, + bindings: [], + downstream: [], + }, + }); + expect( + yield* makePlan( + Effect.gen(function* () { + yield* Function("Worker", { + name: "worker", + }); + }), + ), + ).toMatchObject({ + resources: { + Worker: { + action: "update", + props: { + name: "worker", + }, + bindings: [], + }, + }, + deletions: { + Secret: { + action: "delete", + state: { + status: "created", + downstream: ["Worker"], + }, + }, + }, + }); + }), +); + +test( + "reject deleting a resource when a surviving consumer still references it", + Effect.gen(function* () { + yield* seed({ + Secret: { + instanceId, + providerVersion: 0, + logicalId: "Secret", + fqn: "Secret", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "secret-value", + }, + attr: { + string: "secret-value", + stringArray: [], + stableString: "Secret", + stableArray: ["Secret"], + replaceString: undefined, + }, + bindings: [], + downstream: ["Worker"], + }, + Worker: { + instanceId, + providerVersion: 0, + logicalId: "Worker", + fqn: "Worker", + namespace: undefined, + resourceType: "Test.Function", + status: "created", + props: { + name: "worker", + env: { + SECRET: "secret-value", + }, + }, + attr: { + name: "worker", + env: { + SECRET: "secret-value", + }, + functionArn: "arn:aws:lambda:us-west-2:084828582823:function:Worker", + }, + bindings: [], + downstream: [], + }, + }); + const malformedStack = { + name: TEST_STACK, + stage: TEST_STAGE, + resources: {}, + bindings: {}, + output: undefined, + }; + + const exit = yield* Effect.exit( + Effect.gen(function* () { + const secret = yield* TestResource("Secret", { + string: "secret-value", + }); + yield* Function("Worker", { + name: "worker", + env: { + SECRET: secret.string, + }, + }); + const stack = yield* Stack.Stack; + delete stack.resources.Secret; + }).pipe(makePlanWithCustomStack(malformedStack)), + ); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const reason = exit.cause.reasons.find(Cause.isFailReason); + expect(reason).toBeDefined(); + expect(reason!.error).toEqual( + new Plan.DeleteResourceHasDownstreamDependencies({ + message: "Resource Secret has downstream dependencies", + resourceId: "Secret", + dependencies: ["Worker"], + }), + ); + } + }), +); + +describe("replace resource when replaceString changes", () => { + const stateResources: Record = { + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + replaceString: "A", + }, + attr: {}, + downstream: [], + bindings: [], + }, + }; + + test( + "noop and replace when replaceString is fully resolved at plan time", + Effect.gen(function* () { + yield* seed(stateResources); + expect( + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "A", + }); + }).pipe(makePlan), + ).toMatchObject({ + resources: { + A: { + action: "noop", + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + + expect( + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "B", + }); + }).pipe(makePlan), + ).toMatchObject({ + resources: { + A: { + action: "replace", + props: { + replaceString: "B", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), + ); + + test( + "force preserves replaces", + Effect.gen(function* () { + yield* seed(stateResources); + expect( + yield* Effect.gen(function* () { + yield* TestResource("A", { + replaceString: "B", + }); + }).pipe((effect) => makePlan(effect, { force: true })), + ).toMatchObject({ + resources: { + A: { + action: "replace", + props: { + replaceString: "B", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), + ); + + test( + "update when replaceString depends on unresolved output (diff short-circuits)", + Effect.gen(function* () { + yield* seed(stateResources); + let B: TestResource; + expect( + yield* Effect.gen(function* () { + B = yield* TestResource("B", { + string: "A", + }); + yield* TestResource("A", { + replaceString: B.string, + }); + }).pipe(makePlan), + ).toMatchObject({ + resources: { + A: { + action: "update", + props: { + replaceString: expect.objectContaining({ + kind: "PropExpr", + identifier: "string", + expr: expect.objectContaining({ + kind: "ResourceExpr", + src: B!, + }), + }), + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), + ); +}); + +test( + "update resource when a binding is added without prop changes", + Effect.gen(function* () { + yield* seed({ + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.BindingTarget", + status: "created", + props: { + name: "target", + }, + attr: { + name: "target", + env: {}, + }, + bindings: [], + downstream: [], + }, + }); + expect( + yield* Effect.gen(function* () { + const target = yield* BindingTarget("A", { + name: "target", + }); + yield* target.bind("TestBinding", { + env: { + FEATURE_FLAG: "on", + }, + }); + }).pipe(makePlan), + ).toMatchObject({ + resources: { + A: { + action: "update", + bindings: [ + { + action: "create", + sid: "TestBinding", + data: { + env: { + FEATURE_FLAG: "on", + }, + }, + }, + ], + state: { + status: "created", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test( + "update resource when a binding is removed without prop changes", + Effect.gen(function* () { + yield* seed({ + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.BindingTarget", + status: "created", + props: { + name: "target", + }, + attr: { + name: "target", + env: { + FEATURE_FLAG: "on", + }, + }, + bindings: [ + { + sid: "TestBinding", + data: { + env: { + FEATURE_FLAG: "on", + }, + }, + }, + ], + downstream: [], + }, + }); + expect( + yield* Effect.gen(function* () { + yield* BindingTarget("A", { + name: "target", + }); + }).pipe(makePlan), + ).toMatchObject({ + resources: { + A: { + action: "update", + bindings: [ + { + action: "delete", + sid: "TestBinding", + data: { + env: { + FEATURE_FLAG: "on", + }, + }, + }, + ], + state: { + status: "created", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test.provider( + "binding removals do not keep reappearing after apply", + (scratch) => + Effect.gen(function* () { + const state = yield* yield* State; + yield* state.set({ + stack: scratch.name, + stage: TEST_STAGE, + fqn: "A", + value: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.BindingTarget", + status: "created", + props: { + name: "target", + }, + attr: { + name: "target", + env: { + FEATURE_FLAG: "on", + }, + }, + bindings: [ + { + sid: "TestBinding", + data: { + env: { + FEATURE_FLAG: "on", + }, + }, + }, + ], + downstream: [], + }, + }); + + yield* scratch.deploy( + Effect.gen(function* () { + yield* BindingTarget("A", { + name: "target", + }); + }), + ); + + expect( + yield* state.get({ + stack: scratch.name, + stage: TEST_STAGE, + fqn: "A", + }), + ).toMatchObject({ + bindings: [], + }); + + expect( + yield* Effect.gen(function* () { + yield* BindingTarget("A", { + name: "target", + }); + }).pipe(makePlan), + ).toMatchObject({ + resources: { + A: { + action: "noop", + bindings: [], + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +describe("duplicate bindings are collapsed by sid before diff", () => { + test( + "dedupeBindings keeps the last occurrence of each sid", + Effect.sync(() => { + const deduped = dedupeBindings([ + { sid: "Shared", data: { env: { K: "first" } } }, + { sid: "Other", data: { env: { K: "x" } } }, + { sid: "Shared", data: { env: { K: "last" } } }, + ]); + + // The duplicated sid retains its first-seen position but takes the + // last value (matching `diffBindings`' `Map`-based collapse). + expect(deduped).toEqual([ + { sid: "Shared", data: { env: { K: "last" } } }, + { sid: "Other", data: { env: { K: "x" } } }, + ]); + }), + ); + + test( + "diff observes a single binding when the same sid is bound twice", + Effect.gen(function* () { + yield* seed({ + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.BindingTarget", + status: "created", + props: { + name: "target", + }, + attr: { + name: "target", + env: {}, + }, + bindings: [], + downstream: [], + }, + }); + + // Capture the exact binding list the provider's `diff` receives. + const observed: ResourceBinding[][] = []; + + const plan = yield* Effect.gen(function* () { + const target = yield* BindingTarget("A", { + name: "target", + }); + // The same sid is recorded twice — mirrors a single KV namespace + // bound to two consumers that both attach it to the same target, + // which pushes a duplicate into `stack.bindings[fqn]`. + yield* target.bind("Shared", { env: { FEATURE_FLAG: "on" } }); + yield* target.bind("Shared", { env: { FEATURE_FLAG: "on" } }); + }).pipe( + makePlan, + Effect.provideService(TestResourceHooks, { + diff: (_id, newBindings) => + Effect.sync(() => { + observed.push(newBindings); + }), + }), + ); + + // Before the fix, `diff` saw the raw duplicate pair (length 2) while + // `reconcile` saw a deduped list — an inconsistency that made hashing + // unstable. Every diff invocation must now see the collapsed list. + expect(observed.length).toBeGreaterThan(0); + for (const seen of observed) { + expect(seen).toHaveLength(1); + expect(seen[0]).toMatchObject({ + sid: "Shared", + data: { env: { FEATURE_FLAG: "on" } }, + }); + } + + // The plan node likewise collapses to a single create binding. + expect(plan.resources.A).toMatchObject({ + action: "update", + bindings: [ + { + action: "create", + sid: "Shared", + data: { env: { FEATURE_FLAG: "on" } }, + }, + ], + }); + }), + ); +}); + +describe("construct namespaces", () => { + test( + "namespaced construct bindings resolve into the plan graph", + Effect.gen(function* () { + const Site = Construct.fn(function* (_id: string, _props: {}) { + const bucket = yield* BindingTarget("Bucket", { + name: "bucket", + }); + const distribution = yield* BindingTarget("Distribution", { + name: "distribution", + }); + yield* bucket.bind("Policy", { + env: { + BUCKET: bucket.string, + DISTRIBUTION: distribution.string, + }, + }); + return { bucket, distribution }; + }); + + const plan = yield* Effect.gen(function* () { + yield* Site("MarketingSite", {}); + }).pipe(makePlan); + + expect(plan).toMatchObject({ + resources: { + "MarketingSite/Bucket": { + action: "create", + bindings: [ + { + action: "create", + sid: "Policy", + data: { + env: { + BUCKET: expect.objectContaining({ + kind: "PropExpr", + identifier: "string", + expr: expect.objectContaining({ + kind: "ResourceExpr", + src: plan.resources["MarketingSite/Bucket"]!.resource, + }), + }), + DISTRIBUTION: expect.objectContaining({ + kind: "PropExpr", + identifier: "string", + expr: expect.objectContaining({ + kind: "ResourceExpr", + src: plan.resources["MarketingSite/Distribution"]! + .resource, + }), + }), + }, + }, + }, + ], + }, + "MarketingSite/Distribution": { + action: "create", + bindings: [], + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), + ); + + test( + "same child logical ids in different constructs do not collide", + Effect.gen(function* () { + const Site = Construct.fn(function* ( + _id: string, + props: { name: string }, + ) { + return yield* Bucket("Bucket", { + name: props.name, + }); + }); + + const plan = yield* Effect.gen(function* () { + yield* Site("MarketingSite", { + name: "marketing-bucket", + }); + yield* Site("DocsSite", { + name: "docs-bucket", + }); + }).pipe(makePlan); + + expect(plan).toMatchObject({ + resources: { + "MarketingSite/Bucket": { + action: "create", + props: { + name: "marketing-bucket", + }, + }, + "DocsSite/Bucket": { + action: "create", + props: { + name: "docs-bucket", + }, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), + ); + + test( + "binding-only cycles inside a construct do not become downstream edges", + Effect.gen(function* () { + const Site = Construct.fn(function* (_id: string, _props: {}) { + const A = yield* BindingTarget("A", { + string: "a-value", + }); + const B = yield* BindingTarget("B", { + string: "b-value", + }); + + yield* A.bind("FromB", { + env: { + PEER: B.string, + }, + }); + yield* B.bind("FromA", { + env: { + PEER: A.string, + }, + }); + + return { A, B }; + }); + + const plan = yield* Effect.gen(function* () { + yield* Site("MarketingSite", {}); + }).pipe(makePlan); + + expect(plan.resources["MarketingSite/A"]?.downstream).toEqual([]); + expect(plan.resources["MarketingSite/B"]?.downstream).toEqual([]); + expect(plan.deletions).toEqual({}); + }), + ); +}); + +const createTestResourceState = (options: { + logicalId: string; + status: ResourceStatus; + props: TestResourceProps; + attr?: {}; +}) => + ({ + instanceId, + providerVersion: 0, + ...options, + resourceType: "Test.TestResource", + attr: options.attr ?? {}, + downstream: [], + bindings: [], + fqn: options.logicalId, + namespace: undefined, + }) as ResourceState; + +const createReplacingState = (options: { + logicalId: string; + props: TestResourceProps; + old: ResourceState; + attr?: {}; +}) => + ({ + ...createTestResourceState({ + logicalId: options.logicalId, + status: "replacing", + props: options.props, + attr: options.attr, + }), + old: options.old, + deleteFirst: false, + }) as Extract; + +const createReplacedState = (options: { + logicalId: string; + props: TestResourceProps; + old: ResourceState; + attr?: {}; +}) => + ({ + ...createTestResourceState({ + logicalId: options.logicalId, + status: "replaced", + props: options.props, + attr: options.attr, + }), + old: options.old, + deleteFirst: false, + }) as Extract; + +const testSimple = ( + title: string, + testCase: { + state: { + status: ResourceStatus; + props: TestResourceProps; + attr?: {}; + old?: Partial; + }; + props: TestResourceProps; + plan?: any; + fail?: string; + }, +) => + test( + title, + Effect.gen(function* () { + yield* seed({ + A: createTestResourceState({ + ...testCase.state, + logicalId: "A", + }), + }); + { + const plan = Effect.gen(function* () { + yield* TestResource("A", testCase.props); + }).pipe(makePlan); + + if (testCase.fail) { + const result = plan.pipe( + Effect.map(() => false), + // @ts-expect-error + Effect.catchTag(testCase.fail, () => Effect.succeed(true)), + Effect.catch(() => Effect.succeed(false)), + ) as Effect.Effect; + if (!result) { + expect.fail(`Expected error '${testCase.fail}`); + } + } else { + expect(yield* plan).toMatchObject({ + resources: { + A: testCase.plan, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + } + } + }), + ); + +describe("prior crash in 'creating' state", () => { + testSimple("create if props unchanged", { + state: { + status: "creating", + props: { + string: "A", + }, + }, + props: { + string: "A", + }, + plan: { + action: "create", + props: { + string: "A", + }, + }, + }); + + testSimple("create if changed props can be updated", { + state: { + status: "creating", + props: { + string: "A", + }, + }, + props: { + string: "B", + }, + plan: { + action: "create", + props: { + string: "B", + }, + }, + }); + + testSimple("replace if changed props cannot be updated", { + state: { + status: "creating", + props: { + replaceString: "A", + }, + }, + props: { + replaceString: "B", + }, + plan: { + action: "replace", + props: { + replaceString: "B", + }, + state: { + status: "creating", + props: { + replaceString: "A", + }, + }, + }, + }); +}); + +describe("prior crash in 'updating' state", () => { + testSimple("update if props unchanged", { + state: { + status: "updating", + props: { + string: "A", + }, + }, + props: { + string: "A", + }, + plan: { + action: "update", + props: { + string: "A", + }, + state: { + status: "updating", + props: { + string: "A", + }, + }, + }, + }); + + testSimple("update if changed props can be updated", { + state: { + status: "updating", + props: { + string: "A", + }, + }, + props: { + string: "B", + }, + plan: { + action: "update", + props: { + string: "B", + }, + state: { + status: "updating", + props: { + string: "A", + }, + }, + }, + }); + + testSimple("replace if changed props can not be updated", { + state: { + status: "updating", + props: { + replaceString: "A", + }, + }, + props: { + replaceString: "B", + }, + plan: { + action: "replace", + props: { + replaceString: "B", + }, + state: { + status: "updating", + props: { + replaceString: "A", + }, + }, + }, + }); +}); + +describe("prior crash in 'replacing' state", () => { + const priorStates = ["created", "creating", "updated", "updating"] as const; + + const testUnchanged = ({ + old, + }: { + old: { + status: ResourceStatus; + }; + }) => + testSimple( + `"continue 'replace' if props are unchanged and previous state is '${old.status}'"`, + { + state: { + status: "replacing", + props: { + string: "A", + }, + old, + }, + props: { + string: "A", + }, + plan: { + action: "replace", + props: { + string: "A", + }, + state: { + status: "replacing", + props: { + string: "A", + }, + old, + }, + }, + }, + ); + + priorStates.forEach((status) => + testUnchanged({ + old: { + status, + }, + }), + ); + + const testMinorChange = ({ + old, + }: { + old: { + status: ResourceStatus; + }; + }) => + testSimple( + `"continue 'replace' if props can be updated and previous state is '${old.status}'"`, + { + state: { + status: "replacing", + props: { + string: "A", + }, + old, + }, + props: { + string: "B", + }, + plan: { + action: "replace", + props: { + string: "B", + }, + state: { + status: "replacing", + props: { + string: "A", + }, + old, + }, + }, + }, + ); + + priorStates.forEach((status) => + testMinorChange({ + old: { + status, + }, + }), + ); + + const testReplacement = ( + title: string, + { + old, + plan, + }: { + old: ResourceState; + plan: any; + }, + ) => + testSimple(title, { + state: { + status: "replacing", + props: { + replaceString: "A", + }, + old, + }, + props: { + replaceString: "B", + }, + plan, + }); + + (["replaced", "replacing"] as const).forEach((status) => + testReplacement( + `continue 'replace' if trying to replace a partially replaced resource in state '${status}'`, + { + old: + status === "replaced" + ? createReplacedState({ + logicalId: "A_old1", + props: { + replaceString: "A1", + }, + old: createTestResourceState({ + logicalId: "A_old0", + status: "created", + props: { + replaceString: "A0", + }, + }), + }) + : createReplacingState({ + logicalId: "A_old1", + props: { + replaceString: "A1", + }, + old: createTestResourceState({ + logicalId: "A_old0", + status: "created", + props: { + replaceString: "A0", + }, + }), + }), + plan: { + action: "replace", + props: { + replaceString: "B", + }, + state: { + status: "replacing", + props: { + replaceString: "A", + }, + old: expect.objectContaining({ + status, + props: { + replaceString: "A1", + }, + old: expect.objectContaining({ + status: "created", + props: { + replaceString: "A0", + }, + }), + }), + }, + }, + }, + ), + ); +}); + +describe("prior crash in 'replaced' state", () => { + (["replaced", "replacing"] as const).forEach((status) => + testSimple( + `continue 'replace' if a replaced resource must be replaced again and previous state is '${status}'`, + { + state: { + status: "replaced", + props: { + replaceString: "A1", + }, + old: + status === "replaced" + ? createReplacedState({ + logicalId: "A_old0", + props: { + replaceString: "A0", + }, + old: createTestResourceState({ + logicalId: "A_old-1", + status: "created", + props: { + replaceString: "A-1", + }, + }), + }) + : createReplacingState({ + logicalId: "A_old0", + props: { + replaceString: "A0", + }, + old: createTestResourceState({ + logicalId: "A_old-1", + status: "created", + props: { + replaceString: "A-1", + }, + }), + }), + }, + props: { + replaceString: "B", + }, + plan: { + action: "replace", + props: { + replaceString: "B", + }, + state: { + status: "replaced", + props: { + replaceString: "A1", + }, + old: expect.objectContaining({ + status, + props: { + replaceString: "A0", + }, + old: expect.objectContaining({ + status: "created", + props: { + replaceString: "A-1", + }, + }), + }), + }, + }, + }, + ), + ); +}); + +describe("prior crash in 'deleting' state", () => { + testSimple( + "create the resource if props are unchanged and the previous state is 'deleting'", + { + state: { + status: "deleting", + props: { + string: "A", + }, + }, + props: { + string: "A", + }, + plan: { + action: "create", + props: { + string: "A", + }, + }, + }, + ); +}); + +test( + "lazy Output queue.queueUrl to Function.env", + Effect.gen(function* () { + let MyQueue: Queue; + let MyFunction: Function; + const plan = yield* Effect.gen(function* () { + MyQueue = yield* Queue("MyQueue"); + MyFunction = yield* Function("MyFunction", { + name: "test-function", + env: { + QUEUE_URL: MyQueue.queueUrl, + }, + }); + }).pipe(makePlan); + expect(plan).toMatchObject({ + resources: { + MyFunction: { + action: "create", + bindings: [], + resource: MyFunction!, + props: { + name: "test-function", + env: { + QUEUE_URL: expect.objectContaining({ + kind: "PropExpr", + identifier: "queueUrl", + expr: expect.objectContaining({ + kind: "ResourceExpr", + src: MyQueue!, + }), + }), + }, + }, + state: undefined, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +test( + "detect that queueUrl will change and pass through the PropExpr instead of old output", + Effect.gen(function* () { + yield* seed({ + MyQueue: { + instanceId, + providerVersion: 0, + logicalId: "MyQueue", + fqn: "MyQueue", + namespace: undefined, + resourceType: "Test.Queue", + status: "created", + props: { + name: "test-queue-old", + }, + attr: { + queueUrl: "https://test.queue.com/test-queue-old", + }, + downstream: [], + bindings: [], + }, + }); + let MyQueue: Queue; + let MyFunction: Function; + const plan = yield* Effect.gen(function* () { + MyQueue = yield* Queue("MyQueue"); + MyFunction = yield* Function("MyFunction", { + name: "test-function", + env: { + QUEUE_URL: MyQueue.queueUrl, + }, + }); + }).pipe(makePlan); + expect(plan).toMatchObject({ + resources: { + MyFunction: { + action: "create", + bindings: [], + resource: MyFunction!, + props: { + name: "test-function", + env: { + QUEUE_URL: expect.objectContaining({ + kind: "PropExpr", + identifier: "queueUrl", + expr: expect.objectContaining({ + kind: "ResourceExpr", + src: MyQueue!, + }), + }), + }, + }, + state: undefined, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), +); + +describe("Outputs should resolve to old values", () => { + const stateResources: Record = { + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "test-string", + stringArray: ["test-string"], + }, + attr: { + string: "test-string", + stringArray: ["test-string"], + }, + downstream: [], + bindings: [], + }, + }; + + const expected = (props: Input.Resolve>) => ({ + resources: { + A: { + action: "noop", + bindings: [], + }, + B: { + action: "create", + bindings: [], + props: props, + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + + const subtest = >( + description: string, + input: (resource: TestResource) => I, + attr: Input.Resolve, + ) => + test( + description, + Effect.gen(function* () { + yield* seed(stateResources); + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + stringArray: ["test-string"], + }); + yield* TestResource("B", input(A)); + }).pipe(makePlan), + ).toMatchObject(expected(attr)); + }), + ); + + subtest( + "string", + (A) => ({ + string: A.string, + }), + { + string: "test-string", + }, + ); + + subtest( + "string.apply(string => undefined)", + (A) => ({ + string: A.string.pipe(Output.map(() => undefined)), + }), + { + string: undefined, + }, + ); + + subtest( + "string.effect(string => Effect.succeed(undefined))", + (A) => ({ + string: A.string.pipe(Output.mapEffect(() => Effect.succeed(undefined))), + }), + { + string: undefined, + }, + ); + + subtest( + "string.flatMap(() => Output.literal(undefined))", + (A) => ({ + string: A.string.pipe(Output.flatMap(() => Output.literal(undefined))), + }), + { + string: undefined, + }, + ); + + subtest( + "string.flatMap(string => A.stringArray.map(([first]) => first))", + (A) => ({ + string: A.string.pipe( + Output.flatMap(() => + A.stringArray.pipe( + Output.map((stringArray) => stringArray[0]!.toUpperCase()), + ), + ), + ), + }), + { + string: "TEST-STRING", + }, + ); + + subtest( + "stringArray[0].toUpperCase()", + (A) => ({ + string: A.stringArray.pipe( + Output.map((stringArray) => stringArray[0]!.toUpperCase()), + ), + }), + { + string: "TEST-STRING", + }, + ); + + subtest( + "resource object", + (A) => ({ + object: A as any, + }), + { + object: { + string: "test-string", + }, + } as any, + ); +}); + +describe("raw Resource refs in props are tracked as upstream dependencies", () => { + test( + "raw Resource passed directly as a prop value populates the upstream's downstream", + Effect.gen(function* () { + const plan = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + yield* TestResource("B", { + object: A as any, + }); + }).pipe(makePlan); + + expect(plan.resources.A!.downstream).toEqual(["B"]); + expect(plan.resources.B!.downstream).toEqual([]); + }), + ); + + test( + "raw Resources nested in arrays/objects are tracked as upstream dependencies", + Effect.gen(function* () { + const plan = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { string: "a-value" }); + const B = yield* TestResource("B", { string: "b-value" }); + yield* TestResource("C", { + stringArray: [A] as any, + object: { ref: B } as any, + }); + }).pipe(makePlan); + + expect(plan.resources.A!.downstream).toEqual(["C"]); + expect(plan.resources.B!.downstream).toEqual(["C"]); + expect(plan.resources.C!.downstream).toEqual([]); + }), + ); +}); + +describe("stable properties should not cause downstream changes", () => { + const subtest = ( + description: string, + input: (A: TestResource) => InputProps, + ) => { + // @ts-expect-error - get the keys + const props = input(Output.of({})); + test( + description, + Effect.gen(function* () { + yield* seed({ + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "test-string-old", + }, + attr: { + string: "test-string-old", + stableString: "A", + stableArray: ["A"], + }, + downstream: [], + bindings: [], + }, + B: { + instanceId, + providerVersion: 0, + logicalId: "B", + fqn: "B", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: Object.fromEntries( + Object.entries({ + string: "A", + stringArray: ["A"], + }).filter(([key]) => key in props), + ), + attr: { + stableString: "A", + }, + downstream: [], + bindings: [], + }, + }); + expect( + yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "test-string", + }); + yield* TestResource("B", input(A)); + }).pipe(makePlan), + ).toMatchObject({ + resources: { + A: { + action: "update", + props: { + string: "test-string", + }, + }, + B: { + action: "noop", + }, + }, + deletions: expect.toSatisfy( + (d: any) => Object.keys(d).length === 0, + "empty object", + ), + }); + }), + ); + }; + + subtest("A.stableString", (A) => ({ + string: A.stableString, + })); + + subtest("A.stableString.apply((string) => string.toUpperCase())", (A) => ({ + string: A.stableString.pipe(Output.map((string) => string.toUpperCase())), + })); + + subtest( + "A.stableString.effect((string) => Effect.succeed(string.toUpperCase()))", + (A) => ({ + string: A.stableString.pipe( + Output.mapEffect((string) => Effect.succeed(string.toUpperCase())), + ), + }), + ); + + subtest( + "A.stableString.flatMap((string) => Output.literal(string.toUpperCase()))", + (A) => ({ + string: A.stableString.pipe( + Output.flatMap((string) => Output.literal(string.toUpperCase())), + ), + }), + ); + + subtest("A.stableArray", (A) => ({ + stringArray: A.stableArray, + })); + + subtest("A.stableArray[0]", (A) => ({ + string: A.stableArray.pipe(Output.map((stableArray) => stableArray[0]!)), + })); + + subtest("A.stableArray[0].apply((string) => string.toUpperCase())", (A) => ({ + string: A.stableArray.pipe( + Output.map((stableArray) => stableArray[0]!.toUpperCase()), + ), + })); + + subtest( + "A.stableArray[0].effect((string) => Effect.succeed(string.toUpperCase()))", + (A) => ({ + string: A.stableArray.pipe( + Output.mapEffect((stableArray) => + Effect.succeed(stableArray[0]!.toUpperCase()), + ), + ), + }), + ); +}); + +describe("unsatisfied cycle detection", () => { + const extractCycleDefect = ( + exit: Exit.Exit, + ): UnsatisfiedResourceCycle | undefined => { + if (!Exit.isFailure(exit)) return undefined; + const die = exit.cause.reasons.find(Cause.isDieReason); + return die?.defect as UnsatisfiedResourceCycle | undefined; + }; + + test( + "binding cycle between resources without precreate dies", + Effect.gen(function* () { + const exit = yield* makePlan( + Effect.gen(function* () { + const A = yield* NoPrecreateBindingTarget("A", { + string: "a-value", + }); + const B = yield* NoPrecreateBindingTarget("B", { + string: "b-value", + }); + + yield* A.bind("FromB", { env: { PEER: B.string } }); + yield* B.bind("FromA", { env: { PEER: A.string } }); + + return { A, B }; + }), + ).pipe(Effect.exit); + + const err = extractCycleDefect(exit); + expect(err).toBeDefined(); + expect(err!._tag).toBe("UnsatisfiedResourceCycle"); + expect(err!.cycle.sort()).toEqual(["A", "B"]); + expect(err!.missingPrecreate.sort()).toEqual(["A", "B"]); + }), + ); + + test( + "binding cycle with all precreate resources succeeds", + Effect.gen(function* () { + const exit = yield* makePlan( + Effect.gen(function* () { + const A = yield* BindingTarget("A", { string: "a-value" }); + const B = yield* BindingTarget("B", { string: "b-value" }); + + yield* A.bind("FromB", { + env: { PEER: B.string }, + }); + yield* B.bind("FromA", { + env: { PEER: A.string }, + }); + + return { A, B }; + }), + ).pipe(Effect.exit); + + expect(Exit.isSuccess(exit)).toBe(true); + }), + ); + + test( + "mixed cycle succeeds when precreate resource breaks it", + Effect.gen(function* () { + const exit = yield* makePlan( + Effect.gen(function* () { + const A = yield* BindingTarget("A", { string: "a-value" }); + const B = yield* NoPrecreateBindingTarget("B", { + string: A.string, + }); + + yield* A.bind("FromB", { + env: { PEER: B.string }, + }); + + return { A, B }; + }), + ).pipe(Effect.exit); + + expect(Exit.isSuccess(exit)).toBe(true); + }), + ); + + test( + "three-node binding cycle dies when none have precreate", + Effect.gen(function* () { + const exit = yield* makePlan( + Effect.gen(function* () { + const A = yield* NoPrecreateBindingTarget("A", { string: "a" }); + const B = yield* NoPrecreateBindingTarget("B", { string: "b" }); + const C = yield* NoPrecreateBindingTarget("C", { string: "c" }); + + yield* A.bind("FromC", { env: { PEER: C.string } }); + yield* B.bind("FromA", { env: { PEER: A.string } }); + yield* C.bind("FromB", { env: { PEER: B.string } }); + + return { A, B, C }; + }), + ).pipe(Effect.exit); + + const err = extractCycleDefect(exit); + expect(err).toBeDefined(); + expect(err!._tag).toBe("UnsatisfiedResourceCycle"); + expect(err!.cycle.sort()).toEqual(["A", "B", "C"]); + expect(err!.missingPrecreate.sort()).toEqual(["A", "B", "C"]); + }), + ); + + test( + "acyclic binding graph succeeds even without precreate", + Effect.gen(function* () { + const exit = yield* makePlan( + Effect.gen(function* () { + const A = yield* NoPrecreateBindingTarget("A", { + string: "a-value", + }); + const B = yield* NoPrecreateBindingTarget("B", { + string: A.string, + }); + + yield* B.bind("FromA", { env: { PEER: A.string } }); + + return { A, B }; + }), + ).pipe(Effect.exit); + + expect(Exit.isSuccess(exit)).toBe(true); + }), + ); +}); + +describe("unresolved plan inputs in diff should conservatively update", () => { + test( + "update when upstream resource is new and downstream news contains exprs", + Effect.gen(function* () { + yield* seed({ + B: { + instanceId, + providerVersion: 0, + logicalId: "B", + fqn: "B", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "old-value", + }, + attr: { + string: "old-value", + stableString: "B", + stableArray: ["B"], + }, + downstream: [], + bindings: [], + }, + }); + const plan = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "hello", + }); + yield* TestResource("B", { + string: A.string, + }); + }).pipe(makePlan); + + expect(plan.resources.A.action).toBe("create"); + expect(plan.resources.B.action).toBe("update"); + }), + ); +}); + +describe("Redacted props/outputs are preserved through plan", () => { + test( + "Redacted prop on a new resource is preserved as a Redacted in the plan", + Effect.gen(function* () { + const plan = yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "x", + redacted: Redacted.make("hunter2"), + }); + }).pipe(makePlan); + + const node: any = plan.resources.A!; + expect(node.action).toBe("create"); + const props = node.props as TestResourceProps; + expect(Redacted.isRedacted(props.redacted)).toBe(true); + expect(Redacted.value(props.redacted!)).toBe("hunter2"); + }), + ); + + test( + "Redacted prop nested inside an array is preserved through the plan", + Effect.gen(function* () { + const plan = yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "x", + redactedArray: [Redacted.make("a"), Redacted.make("b")], + }); + }).pipe(makePlan); + + const node: any = plan.resources.A!; + expect(node.action).toBe("create"); + const props = node.props as TestResourceProps; + expect(props.redactedArray).toBeDefined(); + expect(props.redactedArray!.length).toBe(2); + expect(Redacted.isRedacted(props.redactedArray![0]!)).toBe(true); + expect(Redacted.isRedacted(props.redactedArray![1]!)).toBe(true); + expect(Redacted.value(props.redactedArray![0]!)).toBe("a"); + expect(Redacted.value(props.redactedArray![1]!)).toBe("b"); + }), + ); + + test( + "no-op when prior state has the same Redacted value", + Effect.gen(function* () { + yield* seed({ + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "x", + redacted: Redacted.make("hunter2"), + }, + attr: { + string: "x", + stringArray: [], + stableString: "A", + stableArray: ["A"], + replaceString: undefined, + redacted: Redacted.make("hunter2"), + redactedArray: undefined, + }, + downstream: [], + bindings: [], + }, + }); + const plan = yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "x", + redacted: Redacted.make("hunter2"), + }); + }).pipe(makePlan); + + expect(plan.resources.A!.action).toBe("noop"); + }), + ); + + test( + "update when Redacted prop value changes", + Effect.gen(function* () { + yield* seed({ + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "x", + redacted: Redacted.make("old"), + }, + attr: { + string: "x", + stringArray: [], + stableString: "A", + stableArray: ["A"], + replaceString: undefined, + redacted: Redacted.make("old"), + redactedArray: undefined, + }, + downstream: [], + bindings: [], + }, + }); + const plan = yield* Effect.gen(function* () { + yield* TestResource("A", { + string: "x", + redacted: Redacted.make("new"), + }); + }).pipe(makePlan); + + expect(plan.resources.A!.action).toBe("update"); + const node: any = plan.resources.A!; + const props = node.props as TestResourceProps; + expect(Redacted.isRedacted(props.redacted)).toBe(true); + expect(Redacted.value(props.redacted!)).toBe("new"); + }), + ); + + test( + "Redacted output flowing into a downstream resource preserves its redaction", + Effect.gen(function* () { + yield* seed({ + A: { + instanceId, + providerVersion: 0, + logicalId: "A", + fqn: "A", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created", + props: { + string: "x", + redacted: Redacted.make("hunter2"), + }, + attr: { + string: "x", + stringArray: [], + stableString: "A", + stableArray: ["A"], + replaceString: undefined, + redacted: Redacted.make("hunter2"), + redactedArray: undefined, + }, + downstream: [], + bindings: [], + }, + }); + const plan = yield* Effect.gen(function* () { + const A = yield* TestResource("A", { + string: "x", + redacted: Redacted.make("hunter2"), + }); + yield* TestResource("B", { + string: "y", + redacted: A.redacted as any, + }); + }).pipe(makePlan); + + const bNode: any = plan.resources.B!; + const bProps = bNode.props as TestResourceProps; + expect(Redacted.isRedacted(bProps.redacted)).toBe(true); + expect(Redacted.value(bProps.redacted!)).toBe("hunter2"); + }), + ); +}); + +describe("engine-level adoption", () => { + // Build a plan, optionally with an explicit AdoptPolicy and a read hook + // that simulates a pre-existing cloud resource. + const ownedAttrs: TestResource["Attributes"] = { + string: "hello", + stringArray: [], + stableString: "Adopted", + stableArray: ["Adopted"], + replaceString: undefined, + redacted: undefined, + redactedArray: undefined, + }; + + const makeAdoptPlan = ( + effect: Effect.Effect, + opts: { + adopt?: boolean; + readHook?: ( + id: string, + ) => Effect.Effect; + }, + ): Effect.Effect, any, State> => + Effect.gen(function* () { + const { name, stage } = yield* resolveStackId; + const hooksLayer = opts.readHook + ? Layer.succeed(TestResourceHooks, { read: opts.readHook }) + : Layer.empty; + const adoptLayer = + opts.adopt === undefined + ? Layer.empty + : Layer.succeed(AdoptPolicy, opts.adopt); + return yield* (effect as Effect.Effect).pipe( + Stack.make({ + name, + providers: Layer.empty, + state: inMemoryState(), + } as any) as any, + Effect.provideService(Stage, stage), + Effect.flatMap((stackSpec: any) => Plan.make(stackSpec)), + Effect.provide(TestLayers()), + Effect.provide(hooksLayer), + Effect.provide(adoptLayer), + ) as Effect.Effect, any, State>; + }) as Effect.Effect, any, State>; + + test( + "owned read result is silently adopted (no AdoptPolicy needed) and forced to update", + Effect.gen(function* () { + const plan = yield* makeAdoptPlan( + Effect.gen(function* () { + yield* TestResource("Adopted", { string: "hello" }); + }), + { readHook: () => Effect.succeed(ownedAttrs) }, + ); + + // Cold-start adoption forces an update so the provider can re-sync + // tags / config against `news` — even when read returns plain + // (owned) attrs, the cloud resource may carry drift the engine + // can't detect from `props` alone. + expect(plan.resources.Adopted!.action).toBe("update"); + + const state = yield* yield* State; + const persisted = yield* state.get({ + stack: TEST_STACK, + stage: TEST_STAGE, + fqn: "Adopted", + }); + expect(persisted?.status).toBe("created"); + expect((persisted as any)?.attr).toMatchObject({ string: "hello" }); + }), + ); + + test( + "Unowned read result + adopt enabled -> takeover forces an update", + Effect.gen(function* () { + const plan = yield* makeAdoptPlan( + Effect.gen(function* () { + yield* TestResource("Adopted", { string: "hello" }); + }), + { + adopt: true, + readHook: () => Effect.succeed(Unowned(ownedAttrs)), + }, + ); + + // Takeover of an Unowned resource forces `update` so the provider's + // update path can rewrite ownership tags / config to match this + // logical id (a plain noop would leave the resource looking + // foreign-owned to subsequent deploys). + expect(plan.resources.Adopted!.action).toBe("update"); + + const state = yield* yield* State; + const persisted = yield* state.get({ + stack: TEST_STACK, + stage: TEST_STAGE, + fqn: "Adopted", + }); + expect(persisted?.status).toBe("created"); + + // The Unowned brand must be fully scrubbed from anything that + // reaches the state store — both via the public `Unowned.is` + // check *and* via direct symbol inspection (in case someone + // accidentally uses `Symbol.for` rather than `Unowned.is`). + const persistedAttr = (persisted as any)?.attr as object; + expect(Unowned.is(persistedAttr)).toBe(false); + expect(Object.getOwnPropertySymbols(persistedAttr).length).toBe(0); + expect(JSON.stringify(persistedAttr)).not.toContain("Unowned"); + }), + ); + + test( + "Unowned read result + adopt disabled -> OwnedBySomeoneElse", + Effect.gen(function* () { + const exit = yield* makeAdoptPlan( + Effect.gen(function* () { + yield* TestResource("Foreign", { string: "hello" }); + }), + { + adopt: false, + readHook: () => Effect.succeed(Unowned(ownedAttrs)), + }, + ).pipe(Effect.exit); + + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const reason = exit.cause.reasons.find(Cause.isFailReason); + expect((reason?.error as any)?._tag).toBe("OwnedBySomeoneElse"); + expect((reason?.error as any)?.resourceType).toBe("Test.TestResource"); + } + }), + ); + + test( + "read returns undefined -> ordinary create", + Effect.gen(function* () { + const plan = yield* makeAdoptPlan( + Effect.gen(function* () { + yield* TestResource("Fresh", { string: "hello" }); + }), + { readHook: () => Effect.succeed(undefined) }, + ); + + expect(plan.resources.Fresh!.action).toBe("create"); + expect(plan.resources.Fresh!.state).toBeUndefined(); + }), + ); + + test( + "providers without a `read` method skip the adoption probe entirely", + Effect.gen(function* () { + // Bucket has no `read` implementation. The engine should fall back + // to a normal `create` action without any side effects. + const plan = yield* makeAdoptPlan( + Effect.gen(function* () { + yield* Bucket("FreshBucket", { name: "fresh" }); + }), + { adopt: true }, + ); + + expect(plan.resources.FreshBucket!.action).toBe("create"); + }), + ); +}); + +describe("RefExpr resolution", () => { + const seedAt = ( + stack: string, + stage: string, + resources: Record, + ) => + Effect.gen(function* () { + const state = yield* yield* State; + for (const [fqn, value] of Object.entries(resources)) { + yield* state.set({ stack, stage, fqn, value }); + } + }); + + const sharedAttr = { + string: "shared-string", + stringArray: ["shared"], + stableString: "shared-stable", + stableArray: ["shared-stable"], + replaceString: undefined, + redacted: undefined, + redactedArray: undefined, + }; + + const sharedResourceState = { + instanceId, + providerVersion: 0, + logicalId: "Shared", + fqn: "Shared", + namespace: undefined, + resourceType: "Test.TestResource", + status: "created" as ResourceStatus, + props: { string: "shared-string" }, + attr: sharedAttr, + bindings: [], + downstream: [], + } as ResourceState; + + test( + "resolves a cross-stage Ref to the seeded resource's attributes", + Effect.gen(function* () { + yield* seedAt(TEST_STACK, "other", { Shared: sharedResourceState }); + const plan = yield* Effect.gen(function* () { + const shared = yield* TestResource.ref("Shared", { stage: "other" }); + yield* TestResource("Consumer", { string: shared.string }); + }).pipe(makePlan); + + expect(plan.resources.Consumer?.action).toBe("create"); + expect((plan.resources.Consumer as any)?.props).toMatchObject({ + string: "shared-string", + }); + }), + ); + + test( + "resolves a cross-stack Ref using the explicit stack option", + Effect.gen(function* () { + yield* seedAt("other-stack", TEST_STAGE, { + Shared: sharedResourceState, + }); + const plan = yield* Effect.gen(function* () { + const shared = yield* TestResource.ref("Shared", { + stack: "other-stack", + }); + yield* TestResource("Consumer", { + string: shared.string, + }); + }).pipe(makePlan); + + expect((plan.resources.Consumer as any)?.props).toMatchObject({ + string: "shared-string", + }); + }), + ); + + test( + "Ref to a resource in the current stack/stage is resolved", + Effect.gen(function* () { + yield* seed({ Shared: sharedResourceState }); + const plan = yield* Effect.gen(function* () { + const shared = yield* TestResource.ref("Shared"); + yield* TestResource("Consumer", { string: shared.string }); + }).pipe(makePlan); + + expect((plan.resources.Consumer as any)?.props).toMatchObject({ + string: "shared-string", + }); + }), + ); + + test( + "missing Ref target dies with InvalidReferenceError", + Effect.gen(function* () { + const exit = yield* Effect.exit( + Effect.gen(function* () { + const shared = yield* TestResource.ref("Ghost", { stage: "other" }); + yield* TestResource("Consumer", { string: shared.string }); + }).pipe(makePlan), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const err = Cause.squash(exit.cause) as Output.InvalidReferenceError; + expect(err._tag).toBe("InvalidReferenceError"); + expect(err.resourceId).toBe("Ghost"); + expect(err.stage).toBe("other"); + } + }), + ); +}); + +describe("StackRefExpr resolution", () => { + const setStackOutput = (stack: string, stage: string, value: unknown) => + Effect.gen(function* () { + const state = yield* yield* State; + yield* state.setOutput({ stack, stage, value }); + }); + + test( + "resolves an Output.stackRef to the persisted stack output", + Effect.gen(function* () { + yield* setStackOutput("Backend", TEST_STAGE, { + url: "https://api.example.com", + }); + const plan = yield* Effect.gen(function* () { + const backend = yield* Output.stackRef<{ url: string }>("Backend"); + yield* TestResource("Consumer", { + string: (backend as any).url, + }); + }).pipe(makePlan); + + expect(plan.resources.Consumer?.action).toBe("create"); + expect((plan.resources.Consumer as any)?.props).toMatchObject({ + string: "https://api.example.com", + }); + }), + ); + + test( + "resolves an explicit stage on the stackRef", + Effect.gen(function* () { + yield* setStackOutput("Backend", "prod", { + url: "https://prod.example.com", + }); + const plan = yield* Effect.gen(function* () { + const backend = yield* Output.stackRef<{ url: string }>("Backend", { + stage: "prod", + }); + yield* TestResource("Consumer", { + string: (backend as any).url, + }); + }).pipe(makePlan); + + expect((plan.resources.Consumer as any)?.props).toMatchObject({ + string: "https://prod.example.com", + }); + }), + ); + + test( + "missing stack output dies with InvalidReferenceError", + Effect.gen(function* () { + const exit = yield* Effect.exit( + Effect.gen(function* () { + const backend = yield* Output.stackRef<{ url: string }>("Backend", { + stage: "ghost", + }); + yield* TestResource("Consumer", { + string: (backend as any).url, + }); + }).pipe(makePlan), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const err = Cause.squash(exit.cause) as Output.InvalidReferenceError; + expect(err._tag).toBe("InvalidReferenceError"); + expect(err.stack).toBe("Backend"); + expect(err.stage).toBe("ghost"); + } + }), + ); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/schema.test.ts b/.repos/alchemy-effect/packages/alchemy/test/schema.test.ts new file mode 100644 index 00000000000..534014e916c --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/schema.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, test } from "vitest"; + +import { + getSetValueAST, + isBooleanSchema, + isListSchema, + isMapSchema, + isNullishSchema, + isNullSchema, + isNumberSchema, + isRecordLikeSchema, + isSetSchema, + isStringSchema, + isUndefinedSchema, +} from "@/Schema"; +import * as S from "effect/Schema"; + +describe("isStringSchema", () => { + test("string", () => { + expect(isStringSchema(S.String)).toBe(true); + }); + test("not string", () => { + expect(isStringSchema(S.Number)).toBe(false); + }); +}); + +describe("isNumberSchema", () => { + test("number", () => { + expect(isNumberSchema(S.Number)).toBe(true); + }); + test("not number", () => { + expect(isNumberSchema(S.String)).toBe(false); + }); +}); + +describe("isMapSchema", () => { + test("map", () => { + expect(isMapSchema(S.ReadonlyMap(S.String, S.String))).toBe(true); + }); + test("not map", () => { + expect(isMapSchema(S.Record(S.String, S.String))).toBe(false); + }); +}); + +describe("isRecordLikeSchema", () => { + for (const [key, value] of Object.entries({ + map: S.ReadonlyMap(S.String, S.String), + struct: S.Struct({ + key: S.String, + value: S.String, + }), + class: class Self extends S.Class("Self")({ + key: S.String, + value: S.String, + }) {}, + })) { + test(key, () => { + expect(isRecordLikeSchema(value)).toBe(true); + }); + } +}); + +describe("isListSchema", () => { + test("list", () => { + expect(isListSchema(S.Array(S.String))).toBe(true); + }); + test("Map type is not list", () => { + expect(isListSchema(S.ReadonlyMap(S.String, S.String))).toBe(false); + }); +}); + +describe("isSetSchema", () => { + test("set", () => { + expect(isSetSchema(S.ReadonlySet(S.String))).toBe(true); + }); + test("getSetValueAST", () => { + const ast = getSetValueAST(S.ReadonlySet(S.String)); + expect(ast).toBeDefined(); + expect(isStringSchema(S.make(ast!))).toBe(true); + }); +}); + +describe("isNullSchema", () => { + test("null", () => { + expect(isNullSchema(S.Null)).toBe(true); + }); + test("undefined", () => { + expect(isNullSchema(S.Undefined)).toBe(false); + }); + test("not null", () => { + expect(isNullSchema(S.String)).toBe(false); + }); +}); + +describe("isUndefinedSchema", () => { + test("undefined", () => { + expect(isUndefinedSchema(S.Undefined)).toBe(true); + }); + test("not undefined", () => { + expect(isUndefinedSchema(S.String)).toBe(false); + }); +}); + +describe("isNullishSchema", () => { + test("null", () => { + expect(isNullishSchema(S.Null)).toBe(true); + }); + test("undefined", () => { + expect(isNullishSchema(S.Undefined)).toBe(true); + }); + test("not nullish", () => { + expect(isNullishSchema(S.String)).toBe(false); + }); +}); + +describe("isBooleanSchema", () => { + test("boolean", () => { + expect(isBooleanSchema(S.Boolean)).toBe(true); + }); + test("not boolean", () => { + expect(isBooleanSchema(S.String)).toBe(false); + }); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/test.resources.ts b/.repos/alchemy-effect/packages/alchemy/test/test.resources.ts new file mode 100644 index 00000000000..3d4f46e2913 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/test.resources.ts @@ -0,0 +1,832 @@ +import { Artifacts } from "@/Artifacts"; +import { isResolved } from "@/Diff.ts"; +import * as Provider from "@/Provider.ts"; +import { Resource, type ResourceBinding } from "@/Resource"; +import * as State from "@/State/index"; +import { isUnknown } from "@/Util/unknown"; +import * as Context from "effect/Context"; +import { Data } from "effect"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Redacted from "effect/Redacted"; + +// Bucket +export type BucketProps = { + name?: string; +}; + +export interface Bucket extends Resource< + "Test.Bucket", + BucketProps, + { + name: string; + bucketArn: string; + } +> {} + +export const Bucket = Resource("Test.Bucket"); + +const bucketProvider = () => + Provider.succeed(Bucket, { + diff: Effect.fn(function* ({ id, news, output }) { + if (!isResolved(news)) return undefined; + }), + reconcile: Effect.fn(function* ({ id, news = {}, output }) { + if (output !== undefined) return output; + return { + name: news.name ?? id, + bucketArn: `arn:test:bucket:us-east-1:123456789:${id}`, + }; + }), + delete: Effect.fn(function* ({ output }) { + return; + }), + }); + +// Queue +export type QueueProps = { + name?: string; +}; + +export interface Queue extends Resource< + "Test.Queue", + QueueProps, + { + name: string; + queueUrl: string; + } +> {} + +export const Queue = Resource("Test.Queue"); + +export const queueProvider = () => + Provider.succeed(Queue, { + diff: Effect.fn(function* ({ id, news = {}, output }) { + if (!isResolved(news)) return undefined; + }), + reconcile: Effect.fn(function* ({ id, news = {} }) { + const name = news.name ?? id; + return { + name, + queueUrl: `https://test.queue.com/${name}`, + }; + }), + delete: Effect.fn(function* ({ output }) {}), + }); + +export type FunctionProps = { + name?: string; + env?: Record; +}; + +export interface Function extends Resource< + "Test.Function", + FunctionProps, + { + name: string; + env: Record; + functionArn: string; + } +> {} + +export const Function = Resource("Test.Function"); + +export const functionProvider = () => + Provider.succeed(Function, { + diff: Effect.fn(function* ({ id, news, output }) { + if (!isResolved(news)) return undefined; + }), + reconcile: Effect.fn(function* ({ id, news = {} }) { + return { + name: news.name ?? id, + env: news.env ?? {}, + functionArn: `arn:aws:lambda:us-west-2:084828582823:function:${id}`, + }; + }), + delete: Effect.fn(function* ({ output }) {}), + }); + +export type BindingTargetProps = { + name?: string; + string?: string; + replaceString?: string; +}; + +export interface BindingTarget extends Resource< + "Test.BindingTarget", + BindingTargetProps, + { + name: string; + string: string; + env: Record; + replaceString: BindingTargetProps["replaceString"]; + }, + { + env?: Record; + } +> {} + +export const BindingTarget = Resource("Test.BindingTarget"); + +export const bindingTargetProvider = () => + Provider.effect( + BindingTarget, + Effect.gen(function* () { + return { + diff: Effect.fn(function* ({ id, news = {}, olds = {}, newBindings }) { + if (!isResolved(news)) return undefined; + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + if (hooks?.diff && isResolved(newBindings)) { + yield* hooks.diff(id, newBindings as ResourceBinding[]); + } + const n = news as BindingTargetProps; + const o = olds as BindingTargetProps; + if (n.replaceString !== o.replaceString) { + return { + action: "replace", + }; + } + if (n.name !== o.name || n.string !== o.string) { + return { + action: "update", + }; + } + return undefined; + }), + precreate: Effect.fn(function* ({ id, news = {} }) { + return { + name: news.name ?? id, + string: news.string ?? id, + env: {}, + replaceString: news.replaceString, + }; + }), + reconcile: Effect.fn(function* ({ id, news = {}, olds, bindings }) { + // The hook routing tracks the engine's create-vs-update intent. + // `olds === undefined` covers greenfield AND replacement-create + // (engine resets olds when minting a new instance), which is + // exactly when the test wants `failOn("X", "create")` to fire. + // `output === undefined` would miss replacements with `precreate` + // because precreate populates `output` before reconcile runs. + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + if (olds === undefined) { + if (hooks?.create) { + yield* hooks.create(id, news as TestResourceProps); + } + } else { + if (hooks?.update) { + yield* hooks.update(id, news as TestResourceProps); + } + } + return { + name: news.name ?? id, + string: news.string ?? id, + env: Object.assign( + {}, + ...bindings.map( + (binding: any) => binding.env ?? binding.data?.env ?? {}, + ), + ), + replaceString: news.replaceString, + }; + }), + delete: Effect.fn(function* ({ id }) { + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + if (hooks?.delete) { + yield* hooks.delete(id); + } + return; + }), + }; + }), + ); + +export type DeletedBindingRegressionProps = { + name?: string; +}; + +export interface DeletedBindingRegressionTarget extends Resource< + "Test.DeletedBindingRegressionTarget", + DeletedBindingRegressionProps, + { + name: string; + env: Record; + }, + { + env?: Record; + } +> {} + +export const DeletedBindingRegressionTarget = + Resource( + "Test.DeletedBindingRegressionTarget", + ); + +export const deletedBindingRegressionProvider = () => + Provider.succeed(DeletedBindingRegressionTarget, { + diff: Effect.fn(function* () {}), + precreate: Effect.fn(function* ({ id, news = {} }) { + return { + name: news.name ?? id, + env: {}, + }; + }), + reconcile: Effect.fn(function* ({ id, news = {}, bindings }) { + return { + name: news.name ?? id, + env: Object.assign( + {}, + ...bindings.map( + (binding: any) => binding.env ?? binding.data?.env ?? {}, + ), + ), + }; + }), + delete: Effect.fn(function* () {}), + }); + +export type ArtifactProbeProps = { + value: string; +}; + +export interface ArtifactProbe extends Resource< + "Test.ArtifactProbe", + ArtifactProbeProps, + { + value: string; + artifactValue: string | undefined; + } +> {} + +export const ArtifactProbe = Resource("Test.ArtifactProbe"); + +export const artifactProbeProvider = () => + Provider.succeed(ArtifactProbe, { + diff: Effect.fn(function* ({ news, olds }) { + const next = news as ArtifactProbeProps; + const prev = olds as ArtifactProbeProps | undefined; + const artifacts = yield* Artifacts; + const previous = yield* artifacts.get("memo"); + if ( + previous !== undefined && + previous !== next.value && + previous !== prev?.value + ) { + return { action: "replace" as const }; + } + yield* artifacts.set("memo", next.value); + return next.value !== prev?.value + ? { action: "update" as const } + : undefined; + }), + reconcile: Effect.fn(function* ({ news }) { + const props = news as ArtifactProbeProps; + const artifacts = yield* Artifacts; + return { + value: props.value, + artifactValue: yield* artifacts.get("memo"), + }; + }), + delete: Effect.fn(function* () {}), + }); + +// TestResource + +export type TestResourceProps = { + string?: string; + stringArray?: string[]; + object?: { + string: string; + }; + replaceString?: string; + redacted?: Redacted.Redacted; + redactedArray?: Redacted.Redacted[]; +}; + +export interface TestResource extends Resource< + "Test.TestResource", + TestResourceProps, + { + string: string; + stringArray: string[]; + stableString: string; + stableArray: string[]; + replaceString: TestResourceProps["replaceString"]; + redacted: Redacted.Redacted | undefined; + redactedArray: Redacted.Redacted[] | undefined; + } +> {} + +export class TestResourceHooks extends Context.Service< + TestResourceHooks, + { + create?: (id: string, props: TestResourceProps) => Effect.Effect; + update?: (id: string, props: TestResourceProps) => Effect.Effect; + delete?: (id: string) => Effect.Effect; + /** + * If provided, invoked from a binding-aware provider's `diff` with the + * exact `newBindings` array the engine handed it. Lets a test assert what + * the plan stage observes (e.g. that duplicates were collapsed by sid). + */ + diff?: ( + id: string, + newBindings: ResourceBinding[], + ) => Effect.Effect; + /** + * If provided, the read hook is invoked for the resource's `read` lifecycle + * operation. Return: + * - attrs (any object) to simulate an existing cloud resource that is + * adoptable + * - `undefined` to simulate a resource that does not exist + * - fail with `OwnedBySomeoneElse` to reject adoption + */ + read?: ( + id: string, + ) => Effect.Effect; + } +>()("TestResourceHooks") {} + +export const TestResource = Resource("Test.TestResource"); + +export const testResourceProvider = () => + Provider.effect( + TestResource, + Effect.gen(function* () { + return { + read: Effect.fn(function* ({ id, output }) { + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + if (hooks?.read) { + return (yield* hooks.read(id)) as any; + } + return output; + }), + diff: Effect.fn(function* ({ id, news = {}, olds = {} }) { + if (!isResolved(news)) return undefined; + const n = news as TestResourceProps; + const o = olds as TestResourceProps; + if (n.replaceString !== o.replaceString) { + return { + action: "replace", + }; + } + const redactedValue = ( + r: Redacted.Redacted | undefined, + ): string | undefined => + r && Redacted.isRedacted(r) ? Redacted.value(r) : undefined; + const redactedArrayValues = ( + arr: Redacted.Redacted[] | undefined, + ): string[] | undefined => arr?.map((r) => Redacted.value(r)); + const oldRedactedArr = redactedArrayValues(o.redactedArray); + const newRedactedArr = redactedArrayValues(n.redactedArray); + return isUnknown(n.string) || + isUnknown(n.stringArray) || + n.string !== o.string || + n.stringArray?.length !== o.stringArray?.length || + !!n.stringArray !== !!o.stringArray || + n.stringArray?.some(isUnknown) || + n.stringArray?.some((s, i) => s !== o.stringArray?.[i]) || + redactedValue(n.redacted) !== redactedValue(o.redacted) || + oldRedactedArr?.length !== newRedactedArr?.length || + !!oldRedactedArr !== !!newRedactedArr || + newRedactedArr?.some((s, i) => s !== oldRedactedArr?.[i]) + ? { + action: "update", + stables: ["stableString", "stableArray"], + } + : undefined; + }), + reconcile: Effect.fn(function* ({ id, news = {}, olds }) { + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + // Branch on `olds` (engine's create-vs-update intent), not + // `output` — replacements arrive with a precreate stub in + // `output` but `olds === undefined`. + if (olds === undefined) { + if (hooks?.create) { + yield* hooks.create(id, news); + } + } else { + if (hooks?.update) { + yield* hooks.update(id, news); + } + } + return { + string: news.string ?? id, + stringArray: news.stringArray ?? [], + stableString: id, + stableArray: [id], + replaceString: news.replaceString, + redacted: news.redacted, + redactedArray: news.redactedArray, + }; + }), + delete: Effect.fn(function* ({ id }) { + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + if (hooks?.delete) { + yield* hooks.delete(id); + } + return; + }), + }; + }), + ); + +// StaticStablesResource - A test resource that has static stables on the provider +// This simulates resources like VPC, Subnet, etc. where certain properties (e.g., vpcId, subnetId) +// are always stable and defined on the provider itself, not returned dynamically by diff() + +export type StaticStablesResourceProps = { + string?: string; + tags?: Record; + replaceString?: string; +}; + +export interface StaticStablesResource extends Resource< + "Test.StaticStablesResource", + StaticStablesResourceProps, + { + string: string; + tags: Record; + stableId: string; + stableArn: string; + replaceString: StaticStablesResourceProps["replaceString"]; + } +> {} + +export class StaticStablesResourceHooks extends Context.Service< + StaticStablesResourceHooks, + { + create?: ( + id: string, + props: StaticStablesResourceProps, + ) => Effect.Effect; + update?: ( + id: string, + props: StaticStablesResourceProps, + ) => Effect.Effect; + delete?: (id: string) => Effect.Effect; + } +>()("StaticStablesResourceHooks") {} + +export const StaticStablesResource = Resource( + "Test.StaticStablesResource", +); + +export const staticStablesResourceProvider = () => + Provider.succeed(StaticStablesResource, { + // KEY DIFFERENCE: Static stables defined on the provider itself + // These are always stable regardless of what diff() returns + stables: ["stableId", "stableArn"], + diff: Effect.fn(function* ({ id, news = {}, olds = {} }) { + if (!isResolved(news)) return undefined; + const n = news as StaticStablesResourceProps; + const o = olds as StaticStablesResourceProps; + // Replace when replaceString changes + if (n.replaceString !== o.replaceString) { + return { action: "replace" }; + } + // For string changes, return update action + if (n.string !== o.string) { + return { action: "update" }; + } + // For tag-only changes, return undefined (no action) + // This simulates the VPC bug: tags changed, arePropsChanged returns true, + // but diff() returns undefined because provider doesn't explicitly handle tags + return undefined; + }), + reconcile: Effect.fn(function* ({ id, news = {}, olds, output }) { + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(StaticStablesResourceHooks), + ); + // Branch on `olds` (engine create vs update intent). Replacements + // pass `output` from the previous generation if any, but engine + // resets `olds` to `undefined` for the new instance. + if (olds === undefined) { + if (hooks?.create) { + yield* hooks.create(id, news); + } + return { + string: news.string ?? id, + tags: news.tags ?? {}, + stableId: output?.stableId ?? `stable-${id}`, + stableArn: + output?.stableArn ?? + (`arn:test:resource:us-east-1:123456789:${id}` as const), + replaceString: news.replaceString, + }; + } + if (hooks?.update) { + yield* hooks.update(id, news); + } + return { + string: news.string ?? id, + tags: news.tags ?? {}, + stableId: output?.stableId ?? `stable-${id}`, + stableArn: + output?.stableArn ?? + (`arn:test:resource:us-east-1:123456789:${id}` as const), + replaceString: news.replaceString, + }; + }), + delete: Effect.fn(function* ({ id, output }) { + yield* Effect.logDebug(output.string); + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(StaticStablesResourceHooks), + ); + if (hooks?.delete) { + yield* hooks.delete(id); + } + return; + }), + }); + +export type PhasedTargetProps = { + desired: string; + replaceKey?: string; +}; + +export interface PhasedTarget extends Resource< + "Test.PhasedTarget", + PhasedTargetProps, + { + stableId: string; + value: string; + env: Record; + replaceKey: string | undefined; + }, + { + env?: Record; + } +> {} + +export const PhasedTarget = Resource("Test.PhasedTarget"); + +const phasedStableId = (replaceKey?: string) => + `stable:${replaceKey ?? "default"}`; + +const mergeBindingEnv = (bindings: Array) => + Object.assign( + {}, + ...bindings.map((binding) => binding.env ?? binding.data?.env ?? {}), + ); + +export const phasedTargetProvider = () => + Provider.effect( + PhasedTarget, + Effect.gen(function* () { + return { + diff: Effect.fn(function* ({ news, olds }) { + if (!isResolved(news)) return undefined; + const n = news as PhasedTargetProps; + const o = olds as PhasedTargetProps; + if (n.replaceKey !== o.replaceKey) { + return { action: "replace" } as const; + } + if (n.desired !== o.desired) { + return { action: "update" } as const; + } + }), + precreate: Effect.fn(function* ({ news }) { + return { + stableId: phasedStableId(news.replaceKey), + value: `pre:${news.desired}`, + env: {}, + replaceKey: news.replaceKey, + }; + }), + reconcile: Effect.fn(function* ({ id, news, olds, bindings }) { + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + // Branch on `olds` not `output`: replacement-create has + // precreate-populated `output` but engine-cleared `olds`. + if (olds === undefined) { + if (hooks?.create) { + yield* hooks.create(id, { + string: news.desired, + replaceString: news.replaceKey, + }); + } + } else { + if (hooks?.update) { + yield* hooks.update(id, { + string: news.desired, + replaceString: news.replaceKey, + }); + } + } + return { + stableId: phasedStableId(news.replaceKey), + value: news.desired, + env: mergeBindingEnv(bindings), + replaceKey: news.replaceKey, + }; + }), + delete: Effect.fn(function* ({ id }) { + const hooks = Option.getOrUndefined( + yield* Effect.serviceOption(TestResourceHooks), + ); + if (hooks?.delete) { + yield* hooks.delete(id); + } + }), + }; + }), + ); + +// NoPrecreateBindingTarget - like BindingTarget but without precreate, +// used to test cycle detection for resources that cannot break cycles. + +export type NoPrecreateBindingTargetProps = { + string?: string; +}; + +export interface NoPrecreateBindingTarget extends Resource< + "Test.NoPrecreateBindingTarget", + NoPrecreateBindingTargetProps, + { + string: string; + env: Record; + }, + { + env?: Record; + } +> {} + +export const NoPrecreateBindingTarget = Resource( + "Test.NoPrecreateBindingTarget", +); + +export const noPrecreateBindingTargetProvider = () => + Provider.succeed(NoPrecreateBindingTarget, { + diff: Effect.fn(function* () {}), + reconcile: Effect.fn(function* ({ id, news = {}, bindings }) { + return { + string: news.string ?? id, + env: Object.assign( + {}, + ...bindings.map( + (binding: any) => binding.env ?? binding.data?.env ?? {}, + ), + ), + }; + }), + delete: Effect.fn(function* () {}), + }); + +// DurationResource — exercises Duration round-tripping through state. +// Input Duration must arrive at reconcile as a real Duration object (so the +// resolver doesn't shred its prototype); output Duration must re-hydrate from +// state on a subsequent deploy as a real Duration object too. + +export type DurationResourceProps = { + timeout: Duration.Duration; +}; + +export interface DurationResource extends Resource< + "Test.DurationResource", + DurationResourceProps, + { + /** Echoes the input Duration so we can assert what reconcile observed. */ + observedTimeout: Duration.Duration; + /** Authoritative Duration the provider produces; persisted to state. */ + computedTimeout: Duration.Duration; + } +> {} + +export const DurationResource = Resource( + "Test.DurationResource", +); + +export const durationResourceProvider = () => + Provider.succeed(DurationResource, { + diff: Effect.fn(function* ({ news }) { + if (!isResolved(news)) return undefined; + return undefined; + }), + reconcile: Effect.fn(function* ({ news }) { + // If `news.timeout` was shredded by the resolver into a plain object, + // these calls would throw or produce nonsense. The test asserts the + // numeric output so a regression surfaces as a failed assertion. + const observed = news.timeout; + const computed = Duration.millis(Duration.toMillis(observed) + 1_000); + return { + observedTimeout: observed, + computedTimeout: computed, + }; + }), + delete: Effect.fn(function* () {}), + }); + +// Layers +export const TestLayers = () => + Layer.mergeAll( + bucketProvider(), + queueProvider(), + functionProvider(), + bindingTargetProvider(), + deletedBindingRegressionProvider(), + artifactProbeProvider(), + testResourceProvider(), + staticStablesResourceProvider(), + phasedTargetProvider(), + noPrecreateBindingTargetProvider(), + durationResourceProvider(), + ); + +export const InMemoryTestLayers = () => + Layer.mergeAll(TestLayers(), State.inMemoryState()); + +// ── Failure injection helpers ────────────────────────────────────────────── +// +// These helpers produce TestResourceHooks records that can be passed to the +// `hook(...)` test combinator to inject failures or defects into specific +// resource lifecycle methods. `failOn` produces typed failures, while +// `dieOn` and `throwOn` produce defects (uncaught/thrown errors). + +export class ResourceFailure extends Data.TaggedError("ResourceFailure")<{ + message: string; +}> { + constructor(message = "Failed to create") { + super({ message }); + } +} + +type LifecycleHook = "create" | "update" | "delete"; + +export type LifecycleHooks = { + create?: (id: string, props: TestResourceProps) => Effect.Effect; + update?: (id: string, props: TestResourceProps) => Effect.Effect; + delete?: (id: string) => Effect.Effect; +}; + +export const failOn = ( + resourceId: string, + hook: LifecycleHook, +): LifecycleHooks => ({ + [hook]: (id: string) => + id === resourceId + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), +}); + +export const failOnMultiple = ( + failures: Array<{ id: string; hook: LifecycleHook }>, +): LifecycleHooks => { + const idsFor = (hook: LifecycleHook) => + failures.filter((f) => f.hook === hook).map((f) => f.id); + const createFailures = idsFor("create"); + const updateFailures = idsFor("update"); + const deleteFailures = idsFor("delete"); + return { + create: (id: string) => + createFailures.includes(id) + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), + update: (id: string) => + updateFailures.includes(id) + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), + delete: (id: string) => + deleteFailures.includes(id) + ? Effect.fail(new ResourceFailure()) + : Effect.succeed(undefined), + }; +}; + +export const dieOn = ( + resourceId: string, + hook: LifecycleHook, + message = `dieOn:${resourceId}:${hook}`, +): LifecycleHooks => ({ + [hook]: (id: string) => + id === resourceId + ? Effect.die(new Error(message)) + : Effect.succeed(undefined), +}); + +export const throwOn = ( + resourceId: string, + hook: LifecycleHook, + message = `throwOn:${resourceId}:${hook}`, +): LifecycleHooks => ({ + [hook]: (id: string) => + Effect.sync(() => { + if (id === resourceId) { + throw new Error(message); + } + }), +}); diff --git a/.repos/alchemy-effect/packages/alchemy/test/types/Agent.ts b/.repos/alchemy-effect/packages/alchemy/test/types/Agent.ts new file mode 100644 index 00000000000..b9c7e3c235a --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/types/Agent.ts @@ -0,0 +1,165 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpBody from "effect/unstable/http/HttpBody"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import { Sandbox } from "./Sandbox.ts"; + +const _agentEff = Effect.gen(function* () { + const agent1 = yield* Agent; + const _binding1 = agent1.getByName(""); + const profile1 = yield* _binding1.getProfile(); + const agent2 = yield* Agent2; + const _binding2 = agent2.getByName(""); + const profile2 = yield* _binding2.getProfile(); + const agent3 = yield* Agent3; + const _binding3 = agent3.getByName(""); + const profile3 = yield* _binding3.getProfile(); +}); + +const _gen = Effect.gen(function* () { + // bind the Sandbox Container to the Agent DO + const sandbox = yield* Cloudflare.bindContainer(Sandbox); + + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + // get the container instance + const container = yield* Cloudflare.start(sandbox, { + enableInternet: true, + }); + + container.getTcpPort(1080); + container.getUser(); + + return { + getProfile: () => state.storage.get("Profile"), + }; + }); +}); + +export const Agent2 = Cloudflare.DurableObjectNamespace( + "Agents", + Effect.gen(function* () { + // bind the Sandbox Container to the Agent DO + const sandbox = yield* Cloudflare.bindContainer(Sandbox); + + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + // get the container instance + const container = yield* Cloudflare.start(sandbox, { + enableInternet: true, + }); + + container.getTcpPort(1080); + container.getUser(); + + return { + getProfile: () => state.storage.get("Profile"), + }; + }); + }), +); + +export class Agent3 extends Cloudflare.DurableObjectNamespace()( + "Agents", + Effect.gen(function* () { + // bind the Sandbox Container to the Agent DO + const sandbox = yield* Cloudflare.bindContainer(Sandbox); + + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + // get the container instance + const container = yield* Cloudflare.start(sandbox, { + enableInternet: true, + }); + + container.getTcpPort(1080); + container.getUser(); + + return { + getProfile: () => state.storage.get("Profile"), + }; + }); + }), +) {} + +export default class Agent extends Cloudflare.DurableObjectNamespace()( + "Agents", + Effect.gen(function* () { + // bind the Sandbox Container to the Agent DO + const sandbox = yield* Cloudflare.bindContainer(Sandbox); + + return Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState; + + // get the container instance + const container = yield* Cloudflare.start(sandbox, { + enableInternet: true, + }); + + const connection = yield* container.getTcpPort(1080); + + const sessions = new Map(); + + for (const socket of yield* state.getWebSockets()) { + const session = socket.deserializeAttachment<{ id: string }>(); + if (session) { + sessions.set(session.id, socket); + } + } + + return { + getProfile: () => state.storage.get("Profile"), + putProfile: Effect.fnUntraced(function* (value: string) { + yield* state.storage.put("Profile", value); + }), + eval: (code: string) => + connection + .fetch( + HttpClientRequest.post("/eval", { + body: HttpBody.text(code), + }), + ) + .pipe( + Effect.flatMap((response) => response.text), + Effect.orDie, + ), + fetch: Effect.gen(function* () { + const [response, socket] = yield* Cloudflare.upgrade(); + const id = "TODO"; + socket.serializeAttachment({ id }); + sessions.set(id, socket); + return response; + }), + webSocketMessage: Effect.fnUntraced(function* ( + socket: Cloudflare.DurableWebSocket, + message: string | Uint8Array, + ) { + const session = socket.deserializeAttachment<{ id: string }>(); + if (!session) return; + const text = + typeof message === "string" + ? message + : new TextDecoder().decode(message); + for (const peer of sessions.values()) { + yield* peer.send(`[${session.id}] ${text}`); + } + }), + webSocketClose: Effect.fnUntraced(function* ( + ws: Cloudflare.DurableWebSocket, + code: number, + reason: string, + _wasClean: boolean, + ) { + const session = ws.deserializeAttachment<{ id: string }>(); + if (session) { + sessions.delete(session.id); + } + yield* ws.close(code, reason); + }), + }; + }); + }), +) {} diff --git a/.repos/alchemy-effect/packages/alchemy/test/types/Api.ts b/.repos/alchemy-effect/packages/alchemy/test/types/Api.ts new file mode 100644 index 00000000000..b59ba23314b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/types/Api.ts @@ -0,0 +1,137 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import Agent from "./Agent.ts"; + +export const Api2 = Cloudflare.Worker( + "Api", + { + main: import.meta.filename, + observability: { + enabled: true, + }, + compatibility: { + flags: ["nodejs_compat"], + }, + }, + Effect.gen(function* () { + const _agents = yield* Agent; + + return { + getUser: () => Effect.succeed({ id: "123", name: "John Doe" } as const), + fetch: Effect.gen(function* () { + return HttpServerResponse.text("Hello World", { status: 200 }); + }), + }; + }), +); + +const _____ = Effect.gen(function* () { + const worker = yield* Api2; + const _url = worker.url; + const _eff = Effect.gen(function* () { + const rpc = yield* Cloudflare.bindWorker(worker); + rpc.getUser(); + }); + + const worker2 = yield* Api; + const _eff2 = Effect.gen(function* () { + const rpc2 = yield* Cloudflare.bindWorker(worker2); + rpc2.getUser(); + }); + + const worker3 = yield* Api3; + const _eff3 = Effect.gen(function* () { + const rpc3 = yield* Cloudflare.bindWorker(worker3); + rpc3.getUser(); + }); +}); + +// declare the Api service with a tag + props +export default class Api extends Cloudflare.Worker()( + "Api", + { + main: import.meta.filename, + observability: { + enabled: true, + }, + compatibility: { + flags: ["nodejs_compat"], + }, + }, + Effect.gen(function* () { + // (Infrastructure dependencies are bound here) + + // bind the Agent DO to the Worker + const agents = yield* Agent; + + return { + getUser: () => Effect.succeed({ id: "123", name: "John Doe" } as const), + fetch: Effect.gen(function* () { + // (Business logic is implemented here and can reference bound infrastructure above) + const request = yield* HttpServerRequest; + if (request.url.startsWith("/connect/")) { + // connect to a Durable Object web socket + const agentId = request.url.split("/").pop()!; + const agent = agents.getByName(agentId); + const response = yield* agent.fetch(request); + return response; + } // else if (request.url.startsWith("/profile/")) { + // // call RPC methods on a Durable Object + // const key = request.url.split("/").pop()!; + // const agent = yield* agents.getByName(key); + // if (request.method == "GET") { + // const item = yield* agent.getProfile(); + // if (item) { + // return HttpServerResponse.text(item); + // } + // } else if (request.method == "PUT") { + // yield* agent.putProfile(yield* request.text); + // return HttpServerResponse.text("OK", { status: 200 }); + // } else { + // return HttpServerResponse.text("Method not allowed", { + // status: 405, + // }); + // } + // } + return HttpServerResponse.text("Hello World", { status: 200 }); + }), + }; + }).pipe( + // Effect.provide( + // Layer.mergeAll( + // // + // // AgentLive, + // ), + // ), + ), +) {} + +export class Api3 extends Cloudflare.Worker< + Api3, + { + getUser: () => Effect.Effect<{ id: string; name: string }>; + } +>()("Api3", { + main: import.meta.filename, + observability: { + enabled: true, + }, +}) {} + +export const Api3Live = Api3.make( + Effect.gen(function* () { + const agent = yield* Agent; + return { + getUser: Effect.fnUntraced(function* () { + const user = agent.getByName(""); + + return { + id: "123", + name: (yield* user.getProfile())!, + }; + }), + }; + }), +); diff --git a/.repos/alchemy-effect/packages/alchemy/test/types/Sandbox.ts b/.repos/alchemy-effect/packages/alchemy/test/types/Sandbox.ts new file mode 100644 index 00000000000..8aac1bba68e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/types/Sandbox.ts @@ -0,0 +1,59 @@ +import * as Cloudflare from "@/Cloudflare"; +import { Stack } from "@/Stack"; +import * as Effect from "effect/Effect"; +import * as Stream from "effect/Stream"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import * as ChildProcess from "effect/unstable/process/ChildProcess"; + +export class Sandbox extends Cloudflare.Container< + Sandbox, + { + getUser: () => Effect.Effect<{ id: string; name: string }>; + } +>()( + "Sandbox", + Stack.useSync((stack) => ({ + main: import.meta.filename, + // handler: "SandboxLive", + instanceType: stack.stage === "prod" ? "standard-1" : "dev", + dockerfile: `FROM alpine:latest`, + })), +) {} + +export const SandboxLive = Sandbox.make( + Effect.gen(function* () { + // bind dependencies + // yield* Cloudflare.Queue() + + // return http effect + return { + getUser: () => Effect.succeed({ id: "123", name: "John Doe" } as const), + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + // upgrade to web socket + const socket = yield* request.upgrade; + const writeMessage = yield* socket.writer; + const cmd = yield* ChildProcess.make("ffmpeg", ["-version"]); + const [exitCode] = yield* Effect.all( + [ + cmd.exitCode, + // pipe stdout to the websocket + cmd.stdout.pipe( + Stream.tap(writeMessage), + Stream.decodeText, + Stream.mkString, + ), + ] as const, + { concurrency: "unbounded" }, + ); + + return HttpServerResponse.empty({ + status: exitCode === 0 ? 200 : 500, + }); + }).pipe(Effect.orDie), + }; + }), +); + +export default SandboxLive; diff --git a/.repos/alchemy-effect/packages/alchemy/test/vitest.setup.ts b/.repos/alchemy-effect/packages/alchemy/test/vitest.setup.ts new file mode 100644 index 00000000000..72e947763d6 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/vitest.setup.ts @@ -0,0 +1,8 @@ +import { config } from "dotenv"; +config({ path: ".env", quiet: true }); + +// Polyfill File constructor for Node.js if not available +if (typeof globalThis.File === "undefined") { + const { File } = require("node:buffer"); + globalThis.File = File; +} diff --git a/.repos/alchemy-effect/packages/alchemy/test/zip.test.ts b/.repos/alchemy-effect/packages/alchemy/test/zip.test.ts new file mode 100644 index 00000000000..5b00348b62b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/test/zip.test.ts @@ -0,0 +1,23 @@ +import { sha256 } from "@/Util/sha256"; +import { zipCode } from "@/Util/zip"; +import * as Effect from "effect/Effect"; +import { expect, test } from "vitest"; + +test("zipCode is deterministic for identical inputs", async () => { + const hash = () => + Effect.runPromise( + zipCode("export default 1", [ + { + path: "index.mjs.map", + content: JSON.stringify({ + version: 3, + sources: ["index.ts"], + }), + }, + ]).pipe(Effect.flatMap(sha256)), + ); + + const first = await hash(); + await new Promise((resolve) => setTimeout(resolve, 1100)); + expect(await hash()).toBe(first); +}); diff --git a/.repos/alchemy-effect/packages/alchemy/tsconfig.bin.json b/.repos/alchemy-effect/packages/alchemy/tsconfig.bin.json new file mode 100644 index 00000000000..52f66120ccb --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/tsconfig.bin.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["bin/**/*.ts"], + "compilerOptions": { + "composite": true, + "outDir": "./bin", + "rootDir": "./bin", + "noEmit": false, + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "paths": { + "@/*": ["./src/*"] + }, + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "./tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/packages/alchemy/tsconfig.bundle.json b/.repos/alchemy-effect/packages/alchemy/tsconfig.bundle.json new file mode 100644 index 00000000000..20d3b1924bd --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/tsconfig.bundle.json @@ -0,0 +1,6 @@ +{ + "include": ["bin"], + "compilerOptions": { + "noEmit": true + } +} diff --git a/.repos/alchemy-effect/packages/alchemy/tsconfig.json b/.repos/alchemy-effect/packages/alchemy/tsconfig.json new file mode 100644 index 00000000000..08e1458f47e --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["package.json", "src/**/*.ts", "src/**/*.tsx", "src/**/*.json"], + "compilerOptions": { + "composite": true, + "noEmit": false, + "outDir": "./lib", + "rootDir": "./src", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "paths": { + "@/*": ["./src/*"] + }, + // Solid is the default JSX runtime in this package; React files opt in per-file. + "jsx": "react-jsx", + "jsxImportSource": "@opentui/solid", + "lib": ["DOM", "DOM.Iterable", "ES2023"] + } +} diff --git a/.repos/alchemy-effect/packages/alchemy/tsconfig.test.json b/.repos/alchemy-effect/packages/alchemy/tsconfig.test.json new file mode 100644 index 00000000000..309de96c75b --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/tsconfig.test.json @@ -0,0 +1,32 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["package.json", "test"], + "exclude": [ + "test/Local/fixtures/rpc-server-entry.ts", + "test/Local/fixtures/rpc-spawner-parent.ts" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "outDir": "./lib", + "rootDir": ".", + "module": "Preserve", + "moduleResolution": "Bundler", + "target": "ESNext", + "paths": { + "@/*": ["./src/*"] + }, + // Solid is the default JSX runtime in this package; React files opt in per-file. + "jsx": "react-jsx", + "jsxImportSource": "@opentui/solid", + "lib": ["DOM", "DOM.Iterable", "ES2023"] + }, + "references": [ + { + "path": "./tsconfig.json" + }, + { + "path": "./tsconfig.bin.json" + } + ] +} diff --git a/.repos/alchemy-effect/packages/alchemy/tsdown.config.ts b/.repos/alchemy-effect/packages/alchemy/tsdown.config.ts new file mode 100644 index 00000000000..bf9d7d3a575 --- /dev/null +++ b/.repos/alchemy-effect/packages/alchemy/tsdown.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from "tsdown"; + +export default [ + // bundle the CLi into a standalone executable + // defineConfig({ + // entry: ["bin/alchemy.ts"], + // format: ["esm"], + // clean: false, + // shims: true, + // outDir: "bin", + // dts: false, + // sourcemap: true, + // outputOptions: { + // inlineDynamicImports: true, + // }, + // noExternal: ["execa", "open", "env-paths"], + // tsconfig: "tsconfig.bundle.json", + // }), + // bundle the dev-mode worker entrypoint. dev.ts spawns this in a child bun + // process; under a published install it loads from node_modules and would + // otherwise need react/ink/pathe at runtime to resolve InkCLI.tsx as source. + // Bundling inlines those so they stay devDependencies (same rationale as + // the cli bundle below). + defineConfig({ + entry: ["bin/exec.ts"], + format: ["esm"], + clean: false, + shims: true, + outDir: "bin", + dts: false, + sourcemap: true, + outputOptions: { + inlineDynamicImports: true, + }, + tsconfig: "tsconfig.bundle.json", + }), +]; diff --git a/.repos/alchemy-effect/packages/better-auth/package.json b/.repos/alchemy-effect/packages/better-auth/package.json new file mode 100644 index 00000000000..523c35fa40d --- /dev/null +++ b/.repos/alchemy-effect/packages/better-auth/package.json @@ -0,0 +1,35 @@ +{ + "name": "@alchemy.run/better-auth", + "version": "2.0.0-beta.49", + "license": "Apache-2.0", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "packages/better-auth" + }, + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "bun pm pack" + }, + "exports": { + ".": { + "bun": "./src/index.ts", + "default": "./lib/index.js", + "types": "./lib/index.d.ts" + } + }, + "dependencies": { + "alchemy": "workspace:*", + "effect": "catalog:" + }, + "peerDependencies": { + "better-auth": "catalog:" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/.repos/alchemy-effect/packages/better-auth/src/BetterAuth.ts b/.repos/alchemy-effect/packages/better-auth/src/BetterAuth.ts new file mode 100644 index 00000000000..e5c35a51745 --- /dev/null +++ b/.repos/alchemy-effect/packages/better-auth/src/BetterAuth.ts @@ -0,0 +1,13 @@ +import type { RuntimeContext } from "alchemy"; +import type { HttpEffect } from "alchemy/Http"; +import { type Auth } from "better-auth"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; + +export class BetterAuth extends Context.Service< + BetterAuth, + { + auth: Effect.Effect, never, RuntimeContext>; + fetch: HttpEffect; + } +>()("BetterAuth") {} diff --git a/.repos/alchemy-effect/packages/better-auth/src/CloudflareD1.ts b/.repos/alchemy-effect/packages/better-auth/src/CloudflareD1.ts new file mode 100644 index 00000000000..66f55fb6f92 --- /dev/null +++ b/.repos/alchemy-effect/packages/better-auth/src/CloudflareD1.ts @@ -0,0 +1,42 @@ +import { Random } from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import { betterAuth as makeBetterAuth } from "better-auth"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { BetterAuth } from "./BetterAuth.ts"; + +export const CloudflareD1 = Layer.effect( + BetterAuth, + Effect.gen(function* () { + const d1 = yield* Cloudflare.D1Database("BetterAuth"); + + const connection = yield* Cloudflare.D1Connection.bind(d1); + + const BETTER_AUTH_SECRET = yield* Random("BETTER_AUTH_SECRET"); + + const betterAuthSecret = yield* BETTER_AUTH_SECRET.text; + + const betterAuth = yield* Effect.gen(function* () { + return makeBetterAuth({ + database: yield* connection.raw, + secret: yield* betterAuthSecret.pipe(Effect.map(Redacted.value)), + }); + }).pipe(Effect.cached); + + return { + auth: betterAuth, + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const auth = yield* betterAuth; + + const response = yield* Effect.promise(() => + auth.handler(request.source as Request), + ); + return HttpServerResponse.fromWeb(response); + }), + }; + }), +).pipe(Layer.provide(Cloudflare.D1ConnectionLive)); diff --git a/.repos/alchemy-effect/packages/better-auth/src/index.ts b/.repos/alchemy-effect/packages/better-auth/src/index.ts new file mode 100644 index 00000000000..292458b3257 --- /dev/null +++ b/.repos/alchemy-effect/packages/better-auth/src/index.ts @@ -0,0 +1,2 @@ +export * from "./BetterAuth.ts"; +export * from "./CloudflareD1.ts"; diff --git a/.repos/alchemy-effect/packages/better-auth/tsconfig.json b/.repos/alchemy-effect/packages/better-auth/tsconfig.json new file mode 100644 index 00000000000..c064d4844a5 --- /dev/null +++ b/.repos/alchemy-effect/packages/better-auth/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "composite": true, + "noEmit": false, + "outDir": "./lib", + "rootDir": "./src" + }, + "references": [ + { + "path": "../alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/packages/pr-package/README.md b/.repos/alchemy-effect/packages/pr-package/README.md new file mode 100644 index 00000000000..430fdec439f --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/README.md @@ -0,0 +1,165 @@ +# @alchemy.run/pr-package + +A self-hostable PR-package service for Cloudflare. Publish ephemeral, tag-addressable npm tarballs (e.g. one per PR commit) and install them with a pretty URL like `https://pkg.ing//`. + +It packages four Cloudflare resources into a single Effect `handler`: + +- **R2** bucket — stores `.tgz` blobs +- **KV** namespace — tag → resourceId index +- **Secrets Store** + a `Random`-generated **bearer token** — gates writes +- **Durable Object** — per-resource download stats and TTL state + +## Install + +```sh +bun add @alchemy.run/pr-package +``` + +## Usage + +The package exposes a `handler(options)` Effect that you wire into a `Cloudflare.Worker` you own. The reason it can't own the worker for you: Cloudflare bundles the worker starting from a single entry file, and `parseAliasUrl` is a JS closure — it has to live in (or be reachable from) your stack file's module graph. So the worker class lives in your project, and the package contributes the routing. + +### Minimum viable + +Two-file pattern, mirroring how `stacks/otel/Ingester.ts` is split out from `stacks/otel.ts`: + +```ts +// stacks/pr-package/Api.ts — the worker entry (main: import.meta.path) +import * as PrPackage from "@alchemy.run/pr-package"; +import * as Cloudflare from "alchemy/Cloudflare"; + +const parseAliasUrl: PrPackage.ParseAliasUrl = (url) => { + // Map any alias host's URL to { pkgName, tag }, or return null to fall through. + // E.g. https://pkg.example.com//: + const segments = url.pathname.split("/").filter(Boolean); + if (segments.length === 2) { + return { pkgName: segments[0]!, tag: segments[1]! }; + } + return null; +}; + +export default class Api extends Cloudflare.Worker()( + "PrPackageWorker", + { + main: import.meta.path, + url: true, + domain: ["pkg.example.com"], + compatibility: { flags: ["nodejs_compat"], date: "2026-03-17" }, + }, + PrPackage.handler({ parseAliasUrl }), +) {} +``` + +```ts +// stacks/pr-package.ts — the stack +import * as PrPackage from "@alchemy.run/pr-package"; +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import Api from "./pr-package/Api.ts"; + +export default Alchemy.Stack( + "PrPackage", + { providers: Cloudflare.providers(), state: Cloudflare.state() }, + Effect.gen(function* () { + const authToken = yield* PrPackage.AuthTokenValue; + const api = yield* Api; + return { + url: api.url.as(), + authToken: authToken.text, // Redacted bearer token for uploads + }; + }), +); +``` + +Deploy: + +```sh +bun alchemy deploy ./stacks/pr-package.ts --stage prod +``` + +The stack output gives you the worker URL and the auto-generated bearer token. Save the token — you'll need it to publish. + +> **Why two files?** Putting the `Worker` class and `Alchemy.Stack(...)` in the same file pulls the alchemy CLI/state-store surface into the worker bundle and breaks at runtime (`No such module "sisteransi"` and similar). Splitting the worker class into its own file keeps the worker bundle minimal. + +### `handler(options)` options + +| Option | Type | Default | Notes | +| --------------- | ----------------------------------- | ---------------- | -------------------------------------------------------------------- | +| `parseAliasUrl` | `(url: URL) => AliasMatch \| null` | `() => null` | Maps any non-`/projects/...` GET to `{ pkgName, tag }` for a 301. | +| `defaultTtl` | `string` (Effect Duration) | `"3 weeks"` | TTL applied when an upload doesn't pass `X-TTL`. | + +`AliasMatch` is `{ pkgName: string; tag: string }`. Returning `null` falls through to the regular `/projects/:pkgName/...` matcher. + +## API + +All routes are scoped by `:pkgName`, which can be scoped (`@scope/name`) or unscoped (`name`) — matches npm package naming. + +### `PUT /projects/:pkgName/packages` — upload + +Headers: +- `Authorization: Bearer ` (required) +- `Content-Type: application/gzip` +- `X-Tags: ` (required) — e.g. `["main","abc1234","abc1234abc1234..."]` +- `X-TTL: ` (optional) — e.g. `"7 hours"`, `"3 weeks"`. Effect `Duration` syntax. +- `Content-Length` (required) + +Body: raw `.tgz` stream. Streamed straight to R2. + +Returns `{ resourceId, project, tags, ttl, expiresAt }`. + +If a tag was already pointing somewhere, the old resource has the tag removed; if it was the resource's last tag, the blob and metadata are deleted. + +### `GET /` — pretty install URL → 301 + +Whenever the path doesn't start with `/projects/`, the request URL is handed to `parseAliasUrl(url)`. If it returns a match, the worker 301s to `/projects/:pkgName/tags/:tag`. Otherwise 404. + +### `GET /projects/:pkgName/tags/:tag` — resolve tag → 302 to blob + +Looks up `tag → resourceId`, records a download in the per-resource Durable Object, and 302s to `/projects/:pkgName/packages/:resourceId`. + +### `GET /projects/:pkgName/packages/:resourceId` — serve blob + +Returns the `.tgz` with `cache-control: public, max-age=31536000, immutable`. No auth required (resourceIds are unguessable UUIDs). + +### `DELETE /projects/:pkgName/tags/:tag` — remove tag + +Auth required. If the tag was the resource's last one, the blob and metadata are also deleted. + +### `GET /projects/:pkgName/packages/:resourceId/stats` — download stats + +Auth required. Returns `{ downloads: { [tag]: number }, totalDownloads: number }`. + +## Publishing from CI + +```sh +bun pm pack --destination . +tgz=$(ls *.tgz) +curl -fsSL --show-error -X PUT \ + "https://pkg.example.com/projects/my-pkg/packages" \ + -H "Authorization: Bearer ${PR_PACKAGE_TOKEN}" \ + -H "X-Tags: $(jq -nc --arg sha "$GITHUB_SHA" --arg short "${GITHUB_SHA:0:7}" '[$short, $sha, "main"]')" \ + -H "Content-Type: application/gzip" \ + --data-binary "@${tgz}" +``` + +Then consumers install with: + +```sh +bun add https://pkg.example.com/projects/my-pkg/tags/abc1234 +# or via parseAliasUrl, e.g.: +bun add https://pkg.example.com/my-pkg/abc1234 +``` + +See `.github/workflows/pr-package.yaml` in this repo for the full pipeline (publish on push/PR sync, sticky comment with install URLs, tag cleanup on PR close). + +## Cleaning up state + +If a deploy errors mid-flight and leaves orphan state: + +```sh +bun alchemy state resources ./your/stack.ts --profile

+bun alchemy state clear ./your/stack.ts --profile

--yes +``` + +Then reconcile any actually-created Cloudflare resources via the dashboard before redeploying. diff --git a/.repos/alchemy-effect/packages/pr-package/package.json b/.repos/alchemy-effect/packages/pr-package/package.json new file mode 100644 index 00000000000..a6c8837a06a --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/package.json @@ -0,0 +1,34 @@ +{ + "name": "@alchemy.run/pr-package", + "version": "2.0.0-beta.49", + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "packages/pr-package" + }, + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "bun pm pack" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "bun": "./src/index.ts", + "worker": "./src/index.ts", + "import": "./lib/index.js" + } + }, + "dependencies": { + "alchemy": "workspace:*", + "effect": "catalog:" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/.repos/alchemy-effect/packages/pr-package/src/AuthToken.ts b/.repos/alchemy-effect/packages/pr-package/src/AuthToken.ts new file mode 100644 index 00000000000..c63e497f5ae --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/src/AuthToken.ts @@ -0,0 +1,18 @@ +import { Random } from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +export const SecretsStore = Cloudflare.SecretsStore("PrPackageSecrets"); + +/** Random-generated bearer token. Yield to read `.text` from your stack. */ +export const AuthTokenValue = Random("PrPackageAuthTokenValue"); + +/** Cloudflare Secret bound to the worker. Internal — yielded by `handler`. */ +export const AuthToken = Effect.gen(function* () { + const store = yield* SecretsStore; + const value = yield* AuthTokenValue; + return yield* Cloudflare.Secret("PrPackageAuthToken", { + store, + value: value.text, + }); +}); diff --git a/.repos/alchemy-effect/packages/pr-package/src/Bucket.ts b/.repos/alchemy-effect/packages/pr-package/src/Bucket.ts new file mode 100644 index 00000000000..11e6c6f8169 --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/src/Bucket.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const Bucket = Cloudflare.R2Bucket("PrPackageBucket"); diff --git a/.repos/alchemy-effect/packages/pr-package/src/PackageStore.ts b/.repos/alchemy-effect/packages/pr-package/src/PackageStore.ts new file mode 100644 index 00000000000..2e2a8810345 --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/src/PackageStore.ts @@ -0,0 +1,78 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; + +interface PackageState { + tags: string[]; + expiresAt: number; + downloads: Record; + totalDownloads: number; +} + +const emptyState: PackageState = { + tags: [], + expiresAt: 0, + downloads: {}, + totalDownloads: 0, +}; + +export default class PackageStore extends Cloudflare.DurableObjectNamespace()( + "PackageStore", + Effect.gen(function* () { + return Effect.gen(function* () { + const doState = yield* Cloudflare.DurableObjectState; + + const getState = Effect.gen(function* () { + const stored = yield* doState.storage.get("state"); + return stored ?? emptyState; + }); + + const setState = (s: PackageState) => doState.storage.put("state", s); + + return { + init: (tags: string[], expiresAt: number) => + Effect.gen(function* () { + const current = yield* getState; + const merged = new Set([...current.tags, ...tags]); + const newState: PackageState = { + tags: [...merged], + expiresAt, + downloads: current.downloads, + totalDownloads: current.totalDownloads, + }; + yield* setState(newState); + }), + + removeTag: (tag: string) => + Effect.gen(function* () { + const current = yield* getState; + const tags = current.tags.filter((t) => t !== tag); + yield* setState({ ...current, tags }); + return { orphaned: tags.length === 0 }; + }), + + recordDownload: (tag: string) => + Effect.gen(function* () { + const current = yield* getState; + const downloads = { ...current.downloads }; + downloads[tag] = (downloads[tag] ?? 0) + 1; + yield* setState({ + ...current, + downloads, + totalDownloads: current.totalDownloads + 1, + }); + }), + + getStats: () => + Effect.gen(function* () { + const current = yield* getState; + return { + downloads: current.downloads, + totalDownloads: current.totalDownloads, + }; + }), + + getState: () => getState, + }; + }); + }), +) {} diff --git a/.repos/alchemy-effect/packages/pr-package/src/TagIndex.ts b/.repos/alchemy-effect/packages/pr-package/src/TagIndex.ts new file mode 100644 index 00000000000..9fc3b11d7ef --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/src/TagIndex.ts @@ -0,0 +1,3 @@ +import * as Cloudflare from "alchemy/Cloudflare"; + +export const TagIndex = Cloudflare.KVNamespace("PrPackageTagIndex"); diff --git a/.repos/alchemy-effect/packages/pr-package/src/Worker.ts b/.repos/alchemy-effect/packages/pr-package/src/Worker.ts new file mode 100644 index 00000000000..fece272eb10 --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/src/Worker.ts @@ -0,0 +1,352 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { + aliasRedirectPath, + type AliasParserOptions, + type ParseAliasUrl, +} from "./aliases.ts"; +import { AuthToken } from "./AuthToken.ts"; +import { Bucket } from "./Bucket.ts"; +import PackageStore from "./PackageStore.ts"; +import { TagIndex } from "./TagIndex.ts"; + +class Unauthorized { + readonly _tag = "Unauthorized"; +} + +export interface HandlerOptions extends AliasParserOptions { + /** Default TTL when X-TTL is not provided on upload. e.g. "3 weeks". */ + defaultTtl?: string; +} + +const bindings = Layer.mergeAll( + Cloudflare.R2BucketBindingLive, + Cloudflare.KVNamespaceBindingLive, + Cloudflare.SecretBindingLive, +); + +/** + * Init effect for a pr-package worker. Pass as the third argument to + * `Cloudflare.Worker()(...)` in your stack file. + * + * The user's stack file must be the worker entry (`main: import.meta.filename`) + * because `parseAliasUrl` is a closure that has to live in the bundle. + * + * @example + * ```ts + * import * as PrPackage from "@alchemy.run/pr-package"; + * + * class Api extends Cloudflare.Worker()( + * "Api", + * { main: import.meta.filename, url: true, ... }, + * PrPackage.handler({ + * parseAliasUrl: (url) => ({ pkgName: "...", tag: "..." }), + * }), + * ) {} + * ``` + */ +export const handler = (options: HandlerOptions = {}) => + Effect.gen(function* () { + const r2 = yield* Cloudflare.R2Bucket.bind(yield* Bucket); + const kv = yield* Cloudflare.KVNamespace.bind(yield* TagIndex); + const authToken = yield* Cloudflare.Secret.bind(yield* AuthToken); + const packages = yield* PackageStore; + + const parseAliasUrl: ParseAliasUrl = options.parseAliasUrl ?? (() => null); + const defaultTtl = options.defaultTtl ?? "3 weeks"; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const host = request.headers.host ?? "localhost"; + const url = new URL(request.url, `https://${host}`); + const path = url.pathname; + const method = request.method; + + const requireAuth = Effect.gen(function* () { + const authHeader = request.headers.authorization; + const expected = yield* authToken; + if (!authHeader || authHeader !== `Bearer ${expected}`) { + return yield* Effect.fail(new Unauthorized()); + } + }); + + // Pretty alias paths 301 → /projects/:pkgName/tags/:tag (relative). + if (method === "GET" && !path.startsWith("/projects/")) { + const match = parseAliasUrl(url); + if (match) { + return HttpServerResponse.fromWeb( + new Response(null, { + status: 301, + headers: { location: aliasRedirectPath(match) }, + }), + ); + } + } + + // Route: /projects/:pkgName/... + const projectMatch = path.match( + /^\/projects\/((?:@|%40)[^/]+\/[^/]+|[^/]+)(\/.*)?$/i, + ); + if (!projectMatch) { + return HttpServerResponse.text("Not Found", { status: 404 }); + } + const project = decodeURIComponent(projectMatch[1]); + const subPath = projectMatch[2] || "/"; + + // --- PUT /projects/:pkgName/packages --- + if (method === "PUT" && subPath === "/packages") { + return yield* Effect.gen(function* () { + yield* requireAuth; + + const tagsRaw = request.headers["x-tags"]; + const ttlRaw = request.headers["x-ttl"]; + const contentLength = Number( + request.headers["content-length"] ?? 0, + ); + + if (!tagsRaw) { + return yield* HttpServerResponse.json( + { error: "X-Tags header is required" }, + { status: 400 }, + ); + } + + let tags: string[]; + try { + tags = JSON.parse(tagsRaw); + if (!Array.isArray(tags) || tags.length === 0) { + return yield* HttpServerResponse.json( + { error: "X-Tags must be a non-empty JSON array of strings" }, + { status: 400 }, + ); + } + } catch { + return yield* HttpServerResponse.json( + { error: "X-Tags must be valid JSON" }, + { status: 400 }, + ); + } + + if (!contentLength) { + return yield* HttpServerResponse.json( + { error: "Content-Length header is required" }, + { status: 400 }, + ); + } + + const ttlStr = ttlRaw || defaultTtl; + const ttlDuration = Duration.fromInput(ttlStr as Duration.Input); + if (ttlDuration._tag === "None") { + return yield* HttpServerResponse.json( + { + error: + "X-TTL must be an Effect Duration string (e.g. '7 hours', '3 weeks', '30 minutes')", + }, + { status: 400 }, + ); + } + const ttlMillis = Duration.toMillis(ttlDuration.value); + if (ttlMillis <= 0) { + return yield* HttpServerResponse.json( + { + error: + "X-TTL must be a positive duration (e.g. '7 hours', '3 weeks')", + }, + { status: 400 }, + ); + } + const resourceId = crypto.randomUUID(); + const expiresAt = Date.now() + ttlMillis; + + for (const tag of tags) { + const oldResourceId = yield* kv.get(`tag:${project}:${tag}`); + if (oldResourceId && oldResourceId !== resourceId) { + const oldStore = packages.getByName(oldResourceId); + const { orphaned } = yield* oldStore + .removeTag(tag) + .pipe(Effect.orDie); + if (orphaned) { + yield* r2.delete(oldResourceId + ".tgz").pipe(Effect.orDie); + yield* kv.delete(`metadata:${oldResourceId}`); + } + } + } + + yield* r2 + .put(resourceId + ".tgz", request.stream, { + contentLength, + }) + .pipe(Effect.orDie); + + for (const tag of tags) { + yield* kv.put(`tag:${project}:${tag}`, resourceId); + } + + yield* kv.put( + `metadata:${resourceId}`, + JSON.stringify({ project, tags, expiresAt }), + ); + + const store = packages.getByName(resourceId); + yield* store.init(tags, expiresAt).pipe(Effect.orDie); + + return yield* HttpServerResponse.json({ + resourceId, + project, + tags, + ttl: ttlStr, + expiresAt, + }); + }).pipe( + Effect.catchTag("Unauthorized", () => + HttpServerResponse.json( + { error: "unauthorized" }, + { status: 401 }, + ), + ), + ); + } + + // --- GET /projects/:pkgName/tags/:tag --- + if (method === "GET" && subPath.startsWith("/tags/")) { + const tag = decodeURIComponent(subPath.slice("/tags/".length)); + const resourceId = yield* kv.get(`tag:${project}:${tag}`); + if (!resourceId) { + return yield* HttpServerResponse.json( + { error: "tag not found" }, + { status: 404 }, + ); + } + + const store = packages.getByName(resourceId); + yield* store.recordDownload(tag).pipe(Effect.orDie); + + const encodedProject = project + .split("/") + .map(encodeURIComponent) + .join("/"); + return HttpServerResponse.fromWeb( + new Response(null, { + status: 302, + headers: { + location: `/projects/${encodedProject}/packages/${resourceId}`, + }, + }), + ); + } + + // --- GET /projects/:pkgName/packages/:resourceId --- + if ( + method === "GET" && + subPath.startsWith("/packages/") && + !subPath.endsWith("/stats") + ) { + const resourceId = subPath.slice("/packages/".length); + const object = yield* r2.get(resourceId + ".tgz").pipe(Effect.orDie); + if (!object) { + return yield* HttpServerResponse.json( + { error: "resource not found" }, + { status: 404 }, + ); + } + + const body = yield* object.arrayBuffer().pipe(Effect.orDie); + return HttpServerResponse.fromWeb( + new Response(body, { + status: 200, + headers: { + "content-type": "application/gzip", + "cache-control": "public, max-age=31536000, immutable", + }, + }), + ); + } + + // --- DELETE /projects/:pkgName/tags/:tag --- + if (method === "DELETE" && subPath.startsWith("/tags/")) { + return yield* Effect.gen(function* () { + yield* requireAuth; + + const tag = decodeURIComponent(subPath.slice("/tags/".length)); + const resourceId = yield* kv.get(`tag:${project}:${tag}`); + if (!resourceId) { + return yield* HttpServerResponse.json( + { error: "tag not found" }, + { status: 404 }, + ); + } + + const store = packages.getByName(resourceId); + const { orphaned } = yield* store.removeTag(tag).pipe(Effect.orDie); + + yield* kv.delete(`tag:${project}:${tag}`); + + if (orphaned) { + yield* r2.delete(resourceId + ".tgz").pipe(Effect.orDie); + yield* kv.delete(`metadata:${resourceId}`); + } + + return yield* HttpServerResponse.json({ ok: true }); + }).pipe( + Effect.catchTag("Unauthorized", () => + HttpServerResponse.json( + { error: "unauthorized" }, + { status: 401 }, + ), + ), + ); + } + + // --- GET /projects/:pkgName/packages/:resourceId/stats --- + if ( + method === "GET" && + subPath.startsWith("/packages/") && + subPath.endsWith("/stats") + ) { + return yield* Effect.gen(function* () { + yield* requireAuth; + + const resourceId = subPath.slice( + "/packages/".length, + -"/stats".length, + ); + const meta = yield* kv.get(`metadata:${resourceId}`); + if (!meta) { + return yield* HttpServerResponse.json( + { error: "resource not found" }, + { status: 404 }, + ); + } + + const store = packages.getByName(resourceId); + const stats = yield* store.getStats().pipe(Effect.orDie); + + return yield* HttpServerResponse.json(stats); + }).pipe( + Effect.catchTag("Unauthorized", () => + HttpServerResponse.json( + { error: "unauthorized" }, + { status: 401 }, + ), + ), + ); + } + + return HttpServerResponse.text("Not Found", { status: 404 }); + }).pipe( + Effect.catch((error: any) => + Effect.succeed( + HttpServerResponse.text( + `Internal Server Error: ${error?.message ?? error?._tag ?? String(error)}`, + { status: 500 }, + ), + ), + ), + ), + }; + }).pipe(Effect.provide(bindings)); diff --git a/.repos/alchemy-effect/packages/pr-package/src/aliases.ts b/.repos/alchemy-effect/packages/pr-package/src/aliases.ts new file mode 100644 index 00000000000..350ca76aaae --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/src/aliases.ts @@ -0,0 +1,27 @@ +/** + * Pretty install URL parsing. + * + * Every non-`/projects/...` GET hands its full `URL` to `parseAliasUrl`, + * which returns `{ pkgName, tag }` to 301 to the canonical + * `/projects/:pkgName/tags/:tag` route, or `null` to fall through. + * + * Defaults to `() => null` — no aliases recognized. + */ + +export interface AliasMatch { + pkgName: string; + tag: string; +} + +export type ParseAliasUrl = (url: URL) => AliasMatch | null; + +export interface AliasParserOptions { + /** Parse a request URL into a package match, or `null` to fall through. */ + parseAliasUrl?: ParseAliasUrl; +} + +const encodePath = (s: string) => + s.split("/").map(encodeURIComponent).join("/"); + +export const aliasRedirectPath = (match: AliasMatch): string => + `/projects/${encodePath(match.pkgName)}/tags/${encodeURIComponent(match.tag)}`; diff --git a/.repos/alchemy-effect/packages/pr-package/src/index.ts b/.repos/alchemy-effect/packages/pr-package/src/index.ts new file mode 100644 index 00000000000..ac86265ca84 --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/src/index.ts @@ -0,0 +1,6 @@ +export * from "./aliases.ts"; +export { AuthToken, AuthTokenValue, SecretsStore } from "./AuthToken.ts"; +export { Bucket } from "./Bucket.ts"; +export { default as PackageStore } from "./PackageStore.ts"; +export { TagIndex } from "./TagIndex.ts"; +export * from "./Worker.ts"; diff --git a/.repos/alchemy-effect/packages/pr-package/tsconfig.json b/.repos/alchemy-effect/packages/pr-package/tsconfig.json new file mode 100644 index 00000000000..c064d4844a5 --- /dev/null +++ b/.repos/alchemy-effect/packages/pr-package/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "composite": true, + "noEmit": false, + "outDir": "./lib", + "rootDir": "./src" + }, + "references": [ + { + "path": "../alchemy/tsconfig.json" + } + ] +} diff --git a/.repos/alchemy-effect/scripts/audit-service.ts b/.repos/alchemy-effect/scripts/audit-service.ts new file mode 100644 index 00000000000..5686af2d0a3 --- /dev/null +++ b/.repos/alchemy-effect/scripts/audit-service.ts @@ -0,0 +1,1413 @@ +#!/usr/bin/env bun +/** + * Spec-Driven Service Audit Script + * + * This script analyzes a distilled AWS service spec and compares it against + * the alchemy implementation to identify gaps in bindings, resources, + * event sources, and helpers. + * + * Usage: + * bun scripts/audit-service.ts dynamodb + * bun scripts/audit-service.ts s3 + * bun scripts/audit-service.ts --json dynamodb + */ + +import * as fs from "node:fs/promises"; +import * as path from "node:path"; + +// ============ Types ============ + +interface Operation { + name: string; + camelCase: string; + pascalCase: string; + category: OperationCategory; + resourceArity: ResourceArity; + impliesResource: boolean; + impliesEventSource: boolean; + implemented: boolean; + registeredInProviders: boolean; + registeredInIndex: boolean; +} + +type OperationCategory = + | "binding" // Data-plane operation that becomes a Binding.Service + | "resource-lifecycle" // create/update/delete operations that imply a Resource + | "event-source" // stream/notification operations + | "helper-candidate" // operations that might become ergonomic helpers + | "internal"; // operations unlikely to be exposed directly + +type ResourceArity = + | 0 // account/service scoped (e.g., ListBuckets, DescribeLimits) + | 1 // single resource scoped (e.g., GetObject(Bucket), GetItem(Table)) + | 2 // fixed multi-resource (e.g., CopyObject(SourceBucket, DestBucket)) + | "n"; // variadic resource set (e.g., ExecuteTransaction(TableA, TableB, ...)) + +interface AuditReport { + service: string; + distilledPath: string; + alchemyPath: string; + bindingTestPath: string; + totalOperations: number; + implementedBindings: Operation[]; + missingBindings: Operation[]; + resourceLifecycleOps: Operation[]; + eventSourceOps: Operation[]; + helperCandidates: Operation[]; + internalOps: Operation[]; + canonicalResources: CanonicalResource[]; + suggestedHelpers: SuggestedHelper[]; + registrationGaps: RegistrationGap[]; + missingBindingTests: string[]; + leastPrivilegeWarnings: LeastPrivilegeWarning[]; +} + +interface CanonicalResource { + name: string; + impliedByOperations: string[]; + hasProvider: boolean; + suggestedBindings: string[]; +} + +interface SuggestedHelper { + name: string; + pattern: string; + basedOn: string[]; + existingExample: string | null; +} + +interface RegistrationGap { + type: "provider" | "index" | "policy"; + file: string; + missing: string[]; +} + +interface LeastPrivilegeWarning { + binding: string; + file: string; + resourceArity: ResourceArity; + message: string; +} + +// ============ Operation Classification Rules ============ + +// Resource lifecycle operations create/update/delete the resource ITSELF (Table, Bucket, Queue) +// NOT operations on items within a resource (deleteItem is a binding, deleteTable is lifecycle) +const RESOURCE_LIFECYCLE_PATTERNS = [ + /^createTable$/, + /^deleteTable$/, + /^updateTable$/, + /^createBucket$/, + /^deleteBucket$/, + /^createQueue$/, + /^deleteQueue$/, + /^createFunction$/, + /^deleteFunction$/, + /^updateFunctionCode$/, + /^updateFunctionConfiguration$/, + /^createStream$/, + /^deleteStream$/, + /^createPipe$/, + /^deletePipe$/, + /^updatePipe$/, + /^createTopic$/, + /^deleteTopic$/, + /^createSchedule$/, + /^deleteSchedule$/, + /^updateSchedule$/, + /^createScheduleGroup$/, + /^deleteScheduleGroup$/, + /^put[A-Z].*(?:Policy|Configuration|Settings)$/, +]; + +const EVENT_SOURCE_PATTERNS = [ + /stream/i, + /kinesis/i, + /notification/i, + /subscription/i, + /^describe.*Stream/, + /^enable.*Stream/, + /^disable.*Stream/, + /StreamingDestination/i, +]; + +// Operations that are both bindings AND helper candidates (will be classified as bindings first) +// These are data-plane operations that might benefit from higher-level wrappers +const HELPER_CANDIDATE_PATTERNS = [ + /^batch/i, + /^transact/i, + /^execute.*Statement/, +]; + +// Core data-plane bindings (these should always be classified as bindings) +const CORE_BINDING_PATTERNS = [ + /^get(?:Item|Object|Message|Record)$/i, + /^put(?:Item|Object|Record)$/i, + /^delete(?:Item|Object|Message)$/i, + /^update(?:Item)$/i, + /^query$/i, + /^scan$/i, + /^send(?:Message|Record)$/i, + /^receive(?:Message)$/i, + /^head(?:Object|Bucket)$/i, + /^list(?:Objects|ObjectsV2)$/i, + /^copy(?:Object)$/i, + /^upload(?:Part)$/i, + /^complete(?:MultipartUpload)$/i, + /^create(?:MultipartUpload)$/i, + /^abort(?:MultipartUpload)$/i, +]; + +const INTERNAL_PATTERNS = [ + /^describe(?:Endpoints|Limits)$/, + /^list(?:Tags|Backups|Exports|Imports|GlobalTables|ContributorInsights)$/, + /^tag/i, + /^untag/i, + /Backup/i, + /Export/i, + /Import/i, + /GlobalTable(?!s$)/i, + /ReplicaAutoScaling/i, + /ContributorInsights/i, + /ResourcePolicy/i, + /ContinuousBackups/i, +]; + +const ZERO_ARITY_PATTERNS = [ + /^list(?:Tables|Buckets|Queues|Functions|Streams)$/i, + /^describe(?:Endpoints|Limits|Account)$/i, +]; + +const FIXED_MULTI_ARITY_PATTERNS = [/^copy/i, /^replicate/i, /^restore.*From/i]; + +const N_ARITY_PATTERNS = [ + /^batch/i, + /^transact/i, + /^execute.*Statement/i, + /^execute.*Transaction/i, +]; + +// ============ Utilities ============ + +function toPascalCase(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function toCamelCase(str: string): string { + return str.charAt(0).toLowerCase() + str.slice(1); +} + +function matchesAnyPattern(name: string, patterns: RegExp[]): boolean { + return patterns.some((p) => p.test(name)); +} + +function classifyOperation( + serviceName: string, + name: string, +): { + category: OperationCategory; + resourceArity: ResourceArity; + impliesResource: boolean; + impliesEventSource: boolean; +} { + if (serviceName === "iam") { + const lifecycleIamOps = new Set([ + "createAccessKey", + "deleteAccessKey", + "updateAccessKey", + "createAccountAlias", + "deleteAccountAlias", + "updateAccountPasswordPolicy", + "deleteAccountPasswordPolicy", + "createGroup", + "deleteGroup", + "updateGroup", + "createInstanceProfile", + "deleteInstanceProfile", + "createLoginProfile", + "deleteLoginProfile", + "updateLoginProfile", + "createOpenIDConnectProvider", + "deleteOpenIDConnectProvider", + "createPolicy", + "deletePolicy", + "createRole", + "deleteRole", + "updateRole", + "createSAMLProvider", + "deleteSAMLProvider", + "updateSAMLProvider", + "deleteServerCertificate", + "updateServerCertificate", + "createServiceSpecificCredential", + "deleteServiceSpecificCredential", + "updateServiceSpecificCredential", + "deleteSigningCertificate", + "updateSigningCertificate", + "deleteSSHPublicKey", + "updateSSHPublicKey", + "createUser", + "deleteUser", + "updateUser", + "createVirtualMFADevice", + "deleteVirtualMFADevice", + ]); + + if (lifecycleIamOps.has(name)) { + return { + category: "resource-lifecycle", + resourceArity: + name === "updateAccountPasswordPolicy" || + name === "deleteAccountPasswordPolicy" + ? 0 + : 1, + impliesResource: true, + impliesEventSource: false, + }; + } + + return { + category: "internal", + resourceArity: matchesAnyPattern(name, ZERO_ARITY_PATTERNS) ? 0 : 1, + impliesResource: false, + impliesEventSource: false, + }; + } + + if (serviceName === "sns") { + if (name === "createTopic" || name === "deleteTopic") { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: false, + }; + } + + if (name === "subscribe" || name === "unsubscribe") { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: true, + }; + } + } + + if (serviceName === "kinesis") { + if ( + name === "registerStreamConsumer" || + name === "deregisterStreamConsumer" + ) { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: false, + }; + } + + if ( + [ + "addTagsToStream", + "decreaseStreamRetentionPeriod", + "deleteResourcePolicy", + "disableEnhancedMonitoring", + "enableEnhancedMonitoring", + "increaseStreamRetentionPeriod", + "mergeShards", + "putResourcePolicy", + "removeTagsFromStream", + "splitShard", + "startStreamEncryption", + "stopStreamEncryption", + "tagResource", + "untagResource", + "updateMaxRecordSize", + "updateShardCount", + "updateStreamMode", + "updateStreamWarmThroughput", + ].includes(name) + ) { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: false, + }; + } + + if ( + [ + "describeAccountSettings", + "describeLimits", + "describeStream", + "describeStreamConsumer", + "describeStreamSummary", + "getRecords", + "getResourcePolicy", + "getShardIterator", + "listShards", + "listStreamConsumers", + "listStreams", + "listTagsForResource", + "putRecord", + "putRecords", + "subscribeToShard", + ].includes(name) + ) { + return { + category: "binding", + resourceArity: matchesAnyPattern(name, ZERO_ARITY_PATTERNS) ? 0 : 1, + impliesResource: false, + impliesEventSource: false, + }; + } + + if (name === "listTagsForStream" || name === "updateAccountSettings") { + return { + category: "internal", + resourceArity: 1, + impliesResource: false, + impliesEventSource: false, + }; + } + } + + if (serviceName === "rds-data") { + return { + category: "binding", + resourceArity: 1, + impliesResource: false, + impliesEventSource: false, + }; + } + + if (serviceName === "secrets-manager") { + if (["createSecret", "deleteSecret", "updateSecret"].includes(name)) { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: false, + }; + } + + if (name === "listSecrets" || name === "getRandomPassword") { + return { + category: "binding", + resourceArity: 0, + impliesResource: false, + impliesEventSource: false, + }; + } + + if ( + [ + "getSecretValue", + "putSecretValue", + "describeSecret", + "listSecretVersionIds", + "getResourcePolicy", + "putResourcePolicy", + "deleteResourcePolicy", + "updateSecretVersionStage", + "validateResourcePolicy", + ].includes(name) + ) { + return { + category: "binding", + resourceArity: 1, + impliesResource: false, + impliesEventSource: false, + }; + } + } + + if (serviceName === "rds") { + if ( + [ + "createDBCluster", + "deleteDBCluster", + "modifyDBCluster", + "enableHttpEndpoint", + "disableHttpEndpoint", + "startDBCluster", + "stopDBCluster", + "rebootDBCluster", + "createDBClusterEndpoint", + "deleteDBClusterEndpoint", + "modifyDBClusterEndpoint", + "createDBClusterParameterGroup", + "deleteDBClusterParameterGroup", + "modifyDBClusterParameterGroup", + "createDBInstance", + "deleteDBInstance", + "modifyDBInstance", + "createDBParameterGroup", + "deleteDBParameterGroup", + "modifyDBParameterGroup", + "createDBProxy", + "deleteDBProxy", + "modifyDBProxy", + "createDBProxyEndpoint", + "deleteDBProxyEndpoint", + "modifyDBProxyEndpoint", + "modifyDBProxyTargetGroup", + "registerDBProxyTargets", + "deregisterDBProxyTargets", + "createDBSubnetGroup", + "deleteDBSubnetGroup", + "modifyDBSubnetGroup", + "createGlobalCluster", + "deleteGlobalCluster", + "modifyGlobalCluster", + ].includes(name) + ) { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: false, + }; + } + + if ( + [ + "describeDBClusters", + "describeDBInstances", + "describeDBSubnetGroups", + "describeDBClusterParameterGroups", + "describeDBParameterGroups", + "describeDBProxies", + "describeDBProxyEndpoints", + "describeDBProxyTargetGroups", + "describeDBProxyTargets", + "listTagsForResource", + ].includes(name) + ) { + return { + category: "binding", + resourceArity: 1, + impliesResource: false, + impliesEventSource: false, + }; + } + } + + if (serviceName === "eventbridge") { + if ( + [ + "createEventBus", + "deleteEventBus", + "updateEventBus", + "putRule", + "deleteRule", + "putPermission", + "removePermission", + ].includes(name) + ) { + return { + category: "resource-lifecycle", + resourceArity: + name === "putPermission" || name === "removePermission" ? 1 : 1, + impliesResource: true, + impliesEventSource: name === "putRule" || name === "deleteRule", + }; + } + + if ( + [ + "describeEventBus", + "listEventBuses", + "describeRule", + "listRules", + "listTargetsByRule", + "listRuleNamesByTarget", + "putEvents", + "testEventPattern", + ].includes(name) + ) { + return { + category: "binding", + resourceArity: + name === "listEventBuses" || name === "testEventPattern" ? 0 : 1, + impliesResource: false, + impliesEventSource: false, + }; + } + } + + if (serviceName === "pipes") { + if ( + [ + "createPipe", + "describePipe", + "updatePipe", + "deletePipe", + "startPipe", + "stopPipe", + ].includes(name) + ) { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: true, + }; + } + + if (["listPipes", "listTagsForResource"].includes(name)) { + return { + category: "binding", + resourceArity: name === "listPipes" ? 0 : 1, + impliesResource: false, + impliesEventSource: false, + }; + } + } + + if (serviceName === "scheduler") { + if ( + [ + "createSchedule", + "getSchedule", + "updateSchedule", + "deleteSchedule", + "createScheduleGroup", + "getScheduleGroup", + "deleteScheduleGroup", + ].includes(name) + ) { + return { + category: "resource-lifecycle", + resourceArity: 1, + impliesResource: true, + impliesEventSource: true, + }; + } + + if ( + ["listSchedules", "listScheduleGroups", "listTagsForResource"].includes( + name, + ) + ) { + return { + category: "binding", + resourceArity: name === "listTagsForResource" ? 1 : 0, + impliesResource: false, + impliesEventSource: false, + }; + } + } + + const impliesEventSource = matchesAnyPattern(name, EVENT_SOURCE_PATTERNS); + const impliesResource = matchesAnyPattern(name, RESOURCE_LIFECYCLE_PATTERNS); + const isCoreBinding = matchesAnyPattern(name, CORE_BINDING_PATTERNS); + + let category: OperationCategory; + if (matchesAnyPattern(name, INTERNAL_PATTERNS)) { + category = "internal"; + } else if (isCoreBinding) { + // Core bindings take priority - these are the main data-plane operations + category = "binding"; + } else if (impliesResource) { + category = "resource-lifecycle"; + } else if (impliesEventSource) { + category = "event-source"; + } else if (matchesAnyPattern(name, HELPER_CANDIDATE_PATTERNS)) { + category = "helper-candidate"; + } else { + category = "binding"; + } + + let resourceArity: ResourceArity; + if (matchesAnyPattern(name, ZERO_ARITY_PATTERNS)) { + resourceArity = 0; + } else if (matchesAnyPattern(name, N_ARITY_PATTERNS)) { + resourceArity = "n"; + } else if (matchesAnyPattern(name, FIXED_MULTI_ARITY_PATTERNS)) { + resourceArity = 2; + } else { + resourceArity = 1; + } + + return { category, resourceArity, impliesResource, impliesEventSource }; +} + +async function inferImplementedResourceArity( + alchemyPath: string, + pascalCase: string, + fallback: ResourceArity, +): Promise { + const file = path.join(alchemyPath, `${pascalCase}.ts`); + + try { + const content = await fs.readFile(file, "utf-8"); + const serviceSignatureMatch = content.match( + /Binding\.Service<[\s\S]*?,\s*(\([\s\S]*?\)\s*=>\s*Effect\.Effect<)/, + ); + + if (!serviceSignatureMatch) { + return fallback; + } + + const signature = serviceSignatureMatch[1]; + + if (/\(\s*\)\s*=>\s*Effect\.Effect\s*Effect\.Effect\s*Effect\.Effect\s*Effect\.Effect { + try { + const content = await fs.readFile(distilledPath, "utf-8"); + const operations: string[] = []; + const regex = /^export const ([a-z][a-zA-Z0-9]*): API\.OperationMethod /^[a-z]/.test(key)) + .sort(); + } +} + +async function getAlchemyFiles(alchemyPath: string): Promise> { + const files = new Set(); + try { + const entries = await fs.readdir(alchemyPath); + for (const entry of entries) { + if (entry.endsWith(".ts") && entry !== "index.ts") { + files.add(entry.replace(".ts", "")); + } + } + } catch { + // Directory doesn't exist + } + return files; +} + +async function getIndexExports(indexPath: string): Promise> { + const exports = new Set(); + try { + const content = await fs.readFile(indexPath, "utf-8"); + const regex = /export \* from ["']\.\/([^"']+)["']/g; + let match; + while ((match = regex.exec(content)) !== null) { + const name = match[1].replace(".ts", "").replace(".js", ""); + exports.add(name); + } + } catch { + // File doesn't exist + } + return exports; +} + +async function getProvidersRegistrations( + providersPath: string, + service: string, +): Promise<{ resources: Set; bindings: Set }> { + const resources = new Set(); + const bindings = new Set(); + + try { + const content = await fs.readFile(providersPath, "utf-8"); + + // Match DynamoDB.TableProvider(), S3.BucketProvider(), etc. + const resourceRegex = new RegExp( + `${service}\\.([A-Z][a-zA-Z0-9]+)Provider\\(\\)`, + "g", + ); + let match; + while ((match = resourceRegex.exec(content)) !== null) { + resources.add(match[1]); + } + + // Match DynamoDB.GetItemPolicyLive, S3.GetObjectPolicyLive, etc. + // Note: The binding name is like "GetItem" and the export is "GetItemPolicyLive" + const bindingRegex = new RegExp( + `${service}\\.([A-Z][a-zA-Z0-9]+)PolicyLive`, + "g", + ); + while ((match = bindingRegex.exec(content)) !== null) { + bindings.add(match[1]); + } + } catch { + // File doesn't exist + } + + return { resources, bindings }; +} + +async function getBindingTestDescribes( + bindingTestPath: string, +): Promise<{ exists: boolean; describes: Set }> { + const describes = new Set(); + + try { + const content = await fs.readFile(bindingTestPath, "utf-8"); + const regex = /describe\(\s*["']([A-Z][A-Za-z0-9]+)["']/g; + + let match; + while ((match = regex.exec(content)) !== null) { + describes.add(match[1]); + } + + return { exists: true, describes }; + } catch { + return { exists: false, describes }; + } +} + +async function getLeastPrivilegeWarnings( + alchemyPath: string, + operations: Operation[], +): Promise { + const warnings: LeastPrivilegeWarning[] = []; + const wildcardResourcePattern = /Resource:\s*\[\s*["']\*["']\s*\]/; + + for (const op of operations) { + if (!op.implemented || op.resourceArity === 0) { + continue; + } + + if ( + op.category !== "binding" && + op.category !== "helper-candidate" && + op.category !== "event-source" + ) { + continue; + } + + const file = path.join(alchemyPath, `${op.pascalCase}.ts`); + try { + const content = await fs.readFile(file, "utf-8"); + if (wildcardResourcePattern.test(content)) { + warnings.push({ + binding: op.pascalCase, + file, + resourceArity: op.resourceArity, + message: + 'Resource-bound binding uses `Resource: ["*"]`; bind the canonical resource(s) explicitly so the policy stays least-privilege.', + }); + } + } catch { + // Ignore missing or unreadable files; other audit checks will surface those. + } + } + + return warnings.sort((a, b) => a.binding.localeCompare(b.binding)); +} + +// ============ Resource Inference ============ + +function inferCanonicalResources( + operations: Operation[], + existingFiles: Set, +): CanonicalResource[] { + const resourceMap = new Map< + string, + { operations: string[]; bindings: string[] } + >(); + + for (const op of operations) { + if (op.category === "resource-lifecycle") { + let resourceName: string | undefined; + + if ( + ["registerStreamConsumer", "deregisterStreamConsumer"].includes( + op.camelCase, + ) + ) { + resourceName = "StreamConsumer"; + } else if (["putRule", "deleteRule"].includes(op.camelCase)) { + resourceName = "Rule"; + } else if (["putPermission", "removePermission"].includes(op.camelCase)) { + resourceName = "Permission"; + } else if ( + [ + "addTagsToStream", + "createStream", + "decreaseStreamRetentionPeriod", + "deleteResourcePolicy", + "deleteStream", + "disableEnhancedMonitoring", + "enableEnhancedMonitoring", + "increaseStreamRetentionPeriod", + "mergeShards", + "putResourcePolicy", + "removeTagsFromStream", + "splitShard", + "startStreamEncryption", + "stopStreamEncryption", + "tagResource", + "untagResource", + "updateMaxRecordSize", + "updateShardCount", + "updateStreamMode", + "updateStreamWarmThroughput", + ].includes(op.camelCase) + ) { + resourceName = "Stream"; + } + + // Extract resource name from operation like createTable -> Table + const match = + resourceName === undefined + ? op.camelCase.match( + /^(create|delete|update|describe)([A-Z][a-zA-Z]+)/, + ) + : undefined; + if (resourceName || match) { + const resolvedResourceName = resourceName ?? match![2]; + if (!resourceMap.has(resolvedResourceName)) { + resourceMap.set(resolvedResourceName, { + operations: [], + bindings: [], + }); + } + resourceMap.get(resolvedResourceName)!.operations.push(op.camelCase); + } + } else if (op.category === "binding" && op.resourceArity === 1) { + // Associate binding with likely resource + // e.g., getItem, putItem, deleteItem -> Table (DynamoDB convention) + // This is heuristic and service-specific + const commonResources = [ + "Table", + "Bucket", + "Queue", + "Stream", + "Function", + ]; + for (const res of commonResources) { + if (!resourceMap.has(res)) { + resourceMap.set(res, { operations: [], bindings: [] }); + } + resourceMap.get(res)!.bindings.push(op.pascalCase); + } + } + } + + const results: CanonicalResource[] = []; + for (const [name, data] of resourceMap) { + if (data.operations.length > 0) { + results.push({ + name, + impliedByOperations: data.operations, + hasProvider: existingFiles.has(name), + suggestedBindings: data.bindings.slice(0, 10), // Limit for readability + }); + } + } + + return results.sort((a, b) => a.name.localeCompare(b.name)); +} + +// ============ Helper Suggestions ============ + +function suggestHelpers( + operations: Operation[], + service: string, +): SuggestedHelper[] { + const suggestions: SuggestedHelper[] = []; + + // Pattern: Stream-based helpers like notifications(bucket), messages(queue), changes(table) + const streamOps = operations.filter((op) => op.impliesEventSource); + if (streamOps.length > 0) { + const helperName = + service.toLowerCase() === "dynamodb" + ? "changes" + : service.toLowerCase() === "sqs" + ? "messages" + : service.toLowerCase() === "s3" + ? "notifications" + : "events"; + + suggestions.push({ + name: `${helperName}(resource)`, + pattern: "Event stream subscription helper", + basedOn: streamOps.map((op) => op.camelCase), + existingExample: + service.toLowerCase() === "s3" + ? "alchemy/src/AWS/S3/BucketNotifications.ts" + : service.toLowerCase() === "sqs" + ? "alchemy/src/AWS/SQS/QueueEventSource.ts" + : null, + }); + } + + // Pattern: Batch operations -> typed batch helpers + const batchOps = operations.filter( + (op) => + op.camelCase.startsWith("batch") || op.camelCase.startsWith("transact"), + ); + if (batchOps.length > 0) { + suggestions.push({ + name: "batch operations", + pattern: "Typed batch/transaction wrappers", + basedOn: batchOps.map((op) => op.camelCase), + existingExample: null, + }); + } + + return suggestions; +} + +// ============ Main Audit Logic ============ + +async function auditService(serviceName: string): Promise { + const serviceNameLower = serviceName.toLowerCase(); + const serviceNameUpper = + serviceName.charAt(0).toUpperCase() + serviceName.slice(1); + + // Map common service names to their distilled paths and alchemy paths + const serviceConfig: Record = + { + dynamodb: { distilled: "dynamodb", alchemy: "DynamoDB" }, + s3: { distilled: "s3", alchemy: "S3" }, + sqs: { distilled: "sqs", alchemy: "SQS" }, + lambda: { distilled: "lambda", alchemy: "Lambda" }, + kinesis: { distilled: "kinesis", alchemy: "Kinesis" }, + ec2: { distilled: "ec2", alchemy: "EC2" }, + ecs: { distilled: "ecs", alchemy: "ECS" }, + cloudfront: { distilled: "cloudfront", alchemy: "CloudFront" }, + cloudwatch: { distilled: "cloudwatch", alchemy: "CloudWatch" }, + eventbridge: { distilled: "eventbridge", alchemy: "EventBridge" }, + iam: { distilled: "iam", alchemy: "IAM" }, + pipes: { distilled: "pipes", alchemy: "Pipes" }, + sns: { distilled: "sns", alchemy: "SNS" }, + scheduler: { distilled: "scheduler", alchemy: "Scheduler" }, + rds: { distilled: "rds", alchemy: "RDS" }, + "rds-data": { distilled: "rds-data", alchemy: "RDSData" }, + "secrets-manager": { + distilled: "secrets-manager", + alchemy: "SecretsManager", + }, + apigateway: { distilled: "api-gateway", alchemy: "ApiGateway" }, + }; + + const config = serviceConfig[serviceNameLower] || { + distilled: serviceNameLower, + alchemy: serviceNameUpper, + }; + + const preferredDistilledPath = path.resolve( + `.vendor/distilled/@distilled.cloud/aws/src/services/${config.distilled}.ts`, + ); + const fallbackDistilledPath = path.resolve( + `vendor/distilled/packages/aws/src/services/${config.distilled}.ts`, + ); + const resolvedDistilledPath = await fs + .access(preferredDistilledPath) + .then(() => preferredDistilledPath) + .catch(() => + fs + .access(fallbackDistilledPath) + .then(() => fallbackDistilledPath) + .catch(() => undefined), + ); + const distilledPath = + resolvedDistilledPath ?? `@distilled.cloud/aws/${config.distilled}`; + const alchemyPath = path.resolve( + `packages/alchemy/src/AWS/${config.alchemy}`, + ); + const bindingTestPath = path.resolve( + `packages/alchemy/test/AWS/${config.alchemy}/Bindings.test.ts`, + ); + const indexPath = path.join(alchemyPath, "index.ts"); + const providersPath = path.resolve("packages/alchemy/src/AWS/Providers.ts"); + + // Extract data + const distilledOps = await extractDistilledOperations( + resolvedDistilledPath ?? preferredDistilledPath, + `@distilled.cloud/aws/${config.distilled}`, + ); + const alchemyFiles = await getAlchemyFiles(alchemyPath); + const indexExports = await getIndexExports(indexPath); + const providerRegs = await getProvidersRegistrations( + providersPath, + config.alchemy, + ); + const bindingTestCoverage = await getBindingTestDescribes(bindingTestPath); + + // Classify operations + const operations: Operation[] = await Promise.all( + distilledOps.map(async (name) => { + const pascalCase = toPascalCase(name); + const classification = classifyOperation(serviceNameLower, name); + const implemented = alchemyFiles.has(pascalCase); + const resourceArity = implemented + ? await inferImplementedResourceArity( + alchemyPath, + pascalCase, + classification.resourceArity, + ) + : classification.resourceArity; + + return { + name, + camelCase: name, + pascalCase, + ...classification, + resourceArity, + implemented, + registeredInProviders: providerRegs.bindings.has(pascalCase), + registeredInIndex: indexExports.has(pascalCase), + }; + }), + ); + + // Group by category + const implementedBindings = operations.filter( + (op) => op.category === "binding" && op.implemented, + ); + const missingBindings = operations.filter( + (op) => op.category === "binding" && !op.implemented, + ); + const resourceLifecycleOps = operations.filter( + (op) => op.category === "resource-lifecycle", + ); + const eventSourceOps = operations.filter( + (op) => op.category === "event-source", + ); + const helperCandidates = operations.filter( + (op) => op.category === "helper-candidate", + ); + const internalOps = operations.filter((op) => op.category === "internal"); + + // Infer resources and helpers + const canonicalResources = inferCanonicalResources(operations, alchemyFiles); + const suggestedHelpers = suggestHelpers(operations, serviceName); + const leastPrivilegeWarnings = await getLeastPrivilegeWarnings( + alchemyPath, + operations, + ); + + // Find registration gaps + const registrationGaps: RegistrationGap[] = []; + + const implementedButNotInIndex = implementedBindings.filter( + (op) => !op.registeredInIndex, + ); + if (implementedButNotInIndex.length > 0) { + registrationGaps.push({ + type: "index", + file: indexPath, + missing: implementedButNotInIndex.map((op) => op.pascalCase), + }); + } + + const implementedButNotInProviders = implementedBindings.filter( + (op) => !op.registeredInProviders, + ); + if (implementedButNotInProviders.length > 0) { + registrationGaps.push({ + type: "policy", + file: providersPath, + missing: implementedButNotInProviders.map( + (op) => `${op.pascalCase}PolicyLive`, + ), + }); + } + + const missingBindingTests = bindingTestCoverage.exists + ? implementedBindings + .filter((op) => !bindingTestCoverage.describes.has(op.pascalCase)) + .map((op) => op.pascalCase) + : implementedBindings.map((op) => op.pascalCase); + + return { + service: serviceName, + distilledPath, + alchemyPath, + bindingTestPath, + totalOperations: operations.length, + implementedBindings, + missingBindings, + resourceLifecycleOps, + eventSourceOps, + helperCandidates, + internalOps, + canonicalResources, + suggestedHelpers, + registrationGaps, + missingBindingTests, + leastPrivilegeWarnings, + }; +} + +// ============ Report Formatting ============ + +function formatReport(report: AuditReport): string { + const lines: string[] = []; + + lines.push(`\n${"=".repeat(80)}`); + lines.push(`SERVICE AUDIT: ${report.service.toUpperCase()}`); + lines.push(`${"=".repeat(80)}\n`); + + lines.push(`Distilled spec: ${report.distilledPath}`); + lines.push(`Alchemy path: ${report.alchemyPath}`); + lines.push(`Binding tests: ${report.bindingTestPath}`); + lines.push(`Total operations in distilled: ${report.totalOperations}\n`); + + // Summary + lines.push(`${"─".repeat(80)}`); + lines.push("SUMMARY"); + lines.push(`${"─".repeat(80)}`); + lines.push( + ` Implemented bindings: ${report.implementedBindings.length}`, + ); + lines.push(` Missing bindings: ${report.missingBindings.length}`); + lines.push( + ` Resource lifecycle ops: ${report.resourceLifecycleOps.length}`, + ); + lines.push(` Event source ops: ${report.eventSourceOps.length}`); + lines.push(` Helper candidates: ${report.helperCandidates.length}`); + lines.push(` Internal ops (skip): ${report.internalOps.length}`); + lines.push( + ` Missing binding tests: ${report.missingBindingTests.length}`, + ); + lines.push( + ` Least-privilege warnings: ${report.leastPrivilegeWarnings.length}`, + ); + lines.push(""); + + // Implemented bindings + if (report.implementedBindings.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("IMPLEMENTED BINDINGS"); + lines.push(`${"─".repeat(80)}`); + for (const op of report.implementedBindings) { + const arity = `[${formatArity(op.resourceArity)}]`; + const regStatus = op.registeredInProviders + ? "✓ registered" + : "⚠ NOT in Providers.ts"; + lines.push(` ✓ ${op.pascalCase}.ts ${arity} ${regStatus}`); + } + lines.push(""); + } + + // Missing bindings (priority list) + if (report.missingBindings.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("MISSING BINDINGS (implement these)"); + lines.push(`${"─".repeat(80)}`); + + // Group by arity + const arity1 = report.missingBindings.filter( + (op) => op.resourceArity === 1, + ); + const arity0 = report.missingBindings.filter( + (op) => op.resourceArity === 0, + ); + const arity2 = report.missingBindings.filter( + (op) => op.resourceArity === 2, + ); + const arityN = report.missingBindings.filter( + (op) => op.resourceArity === "n", + ); + + if (arity1.length > 0) { + lines.push(" Single-resource bindings (arity=1):"); + for (const op of arity1.slice(0, 20)) { + lines.push(` • ${op.pascalCase} (${op.camelCase})`); + } + if (arity1.length > 20) { + lines.push(` ... and ${arity1.length - 20} more`); + } + } + + if (arity0.length > 0) { + lines.push(" Service-scoped bindings (arity=0):"); + for (const op of arity0) { + lines.push(` • ${op.pascalCase} (${op.camelCase})`); + } + } + + if (arity2.length > 0) { + lines.push(" Fixed multi-resource bindings (arity=2):"); + for (const op of arity2) { + lines.push(` • ${op.pascalCase} (${op.camelCase})`); + } + } + + if (arityN.length > 0) { + lines.push(" Variadic resource bindings (arity=n):"); + for (const op of arityN) { + lines.push(` • ${op.pascalCase} (${op.camelCase})`); + } + } + lines.push(""); + } + + // Canonical resources + if (report.canonicalResources.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("CANONICAL RESOURCES (IaC resources to implement)"); + lines.push(`${"─".repeat(80)}`); + for (const res of report.canonicalResources) { + const status = res.hasProvider ? "✓ has provider" : "⚠ MISSING provider"; + lines.push(` ${res.name}: ${status}`); + lines.push(` Implied by: ${res.impliedByOperations.join(", ")}`); + } + lines.push(""); + } + + // Event source operations + if (report.eventSourceOps.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("EVENT SOURCE OPERATIONS"); + lines.push(`${"─".repeat(80)}`); + for (const op of report.eventSourceOps) { + const impl = op.implemented ? "✓" : "○"; + lines.push(` ${impl} ${op.camelCase}`); + } + lines.push(""); + } + + // Helper suggestions + if (report.suggestedHelpers.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("SUGGESTED HELPERS"); + lines.push(`${"─".repeat(80)}`); + for (const helper of report.suggestedHelpers) { + lines.push(` ${helper.name}`); + lines.push(` Pattern: ${helper.pattern}`); + lines.push(` Based on: ${helper.basedOn.join(", ")}`); + if (helper.existingExample) { + lines.push(` Example: ${helper.existingExample}`); + } + } + lines.push(""); + } + + // Registration gaps + if (report.registrationGaps.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("REGISTRATION GAPS (fix these)"); + lines.push(`${"─".repeat(80)}`); + for (const gap of report.registrationGaps) { + lines.push(` ${gap.type.toUpperCase()} (${gap.file}):`); + for (const item of gap.missing) { + lines.push(` • ${item}`); + } + } + lines.push(""); + } + + if (report.missingBindingTests.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("MISSING BINDING TESTS (add describe blocks)"); + lines.push(`${"─".repeat(80)}`); + lines.push(` In: ${report.bindingTestPath}`); + for (const binding of report.missingBindingTests) { + lines.push(` • describe("${binding}", ...)`); + } + lines.push(""); + } + + if (report.leastPrivilegeWarnings.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("LEAST-PRIVILEGE WARNINGS"); + lines.push(`${"─".repeat(80)}`); + for (const warning of report.leastPrivilegeWarnings) { + lines.push( + ` ⚠ ${warning.binding}.ts [${formatArity(warning.resourceArity)}] (${warning.file})`, + ); + lines.push(` ${warning.message}`); + } + lines.push(""); + } + + // Helper candidate operations + if (report.helperCandidates.length > 0) { + lines.push(`${"─".repeat(80)}`); + lines.push("HELPER CANDIDATE OPERATIONS (consider wrapping)"); + lines.push(`${"─".repeat(80)}`); + for (const op of report.helperCandidates) { + const impl = op.implemented ? "✓" : "○"; + lines.push( + ` ${impl} ${op.camelCase} [${formatArity(op.resourceArity)}]`, + ); + } + lines.push(""); + } + + lines.push(`${"=".repeat(80)}`); + lines.push("END OF AUDIT"); + lines.push(`${"=".repeat(80)}\n`); + + return lines.join("\n"); +} + +// ============ CLI ============ + +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.log("Usage: bun scripts/audit-service.ts [--json] "); + console.log("Example: bun scripts/audit-service.ts dynamodb"); + process.exit(1); + } + + const jsonOutput = args.includes("--json"); + const serviceName = args.filter((a) => !a.startsWith("--"))[0]; + + if (!serviceName) { + console.error("Error: No service name provided"); + process.exit(1); + } + + try { + const report = await auditService(serviceName); + + if (jsonOutput) { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(formatReport(report)); + } + } catch (error) { + console.error(`Error auditing service ${serviceName}:`, error); + process.exit(1); + } +} + +main(); diff --git a/.repos/alchemy-effect/scripts/cleanup-apigateway-rest-apis.sh b/.repos/alchemy-effect/scripts/cleanup-apigateway-rest-apis.sh new file mode 100755 index 00000000000..1f6f8f68abd --- /dev/null +++ b/.repos/alchemy-effect/scripts/cleanup-apigateway-rest-apis.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# +# Deletes API Gateway REST APIs one-by-one, retrying each delete indefinitely. +# Intended for severely throttled accounts (TooManyRequests) after test runs. +# +# Usage: +# ./scripts/cleanup-apigateway-rest-apis.sh +# # deletes every REST API whose name contains "Ag" (adjust FILTER_QUERY) +# ./scripts/cleanup-apigateway-rest-apis.sh /path/to/ids.txt +# # one rest-api-id per line +# +# Environment: +# FILTER_QUERY Override JMESPath for get-rest-apis (default matches `Ag`) +# SLEEP_BETWEEN_APIS_SECONDS Pause after each successful delete (default 2) +# AWS_PROFILE / AWS_REGION Standard aws-cli +# + +set -u + +# Let this script own retry/backoff; each nested SDK retry wastes throttle tokens. +export AWS_MAX_ATTEMPTS=${AWS_MAX_ATTEMPTS:-1} +export AWS_RETRY_MODE=${AWS_RETRY_MODE:-standard} + +FILTER_QUERY="${FILTER_QUERY:-items[?contains(name, \`Ag\`)].id}" + +# AWS DeleteRestApi has a hard 30s/account throttle. Match it on the inner +# retry so we don't burn tokens with sub-30s backoff. +sleep_between_apis_seconds="${SLEEP_BETWEEN_APIS_SECONDS:-32}" +initial_retry_seconds="${INITIAL_RETRY_SECONDS:-32}" +max_retry_seconds="${MAX_RETRY_SECONDS:-120}" + +ts() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +delete_one_until_ok() { + local id="$1" + local attempt=0 + + [[ -z "$id" ]] && return + + local err + while true; do + attempt=$((attempt + 1)) + + if err=$(aws apigateway delete-rest-api --rest-api-id "$id" 2>&1); then + printf '%s deleted %s (attempt %d)\n' "$(ts)" "$id" "$attempt" + return 0 + fi + + local wait_sec=$initial_retry_seconds + if ((wait_sec > max_retry_seconds)); then + wait_sec=$max_retry_seconds + fi + + printf '%s delete %s failed; retry in %ds | %s\n' \ + "$(ts)" "$id" "$wait_sec" \ + "$(echo "$err" | tr '\n' ' ')" >&2 + + sleep "$wait_sec" + done +} + +list_ids_from_aws() { + aws apigateway get-rest-apis \ + --query "$FILTER_QUERY" \ + --output text \ + || die "get-rest-apis failed" +} + +main() { + if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + sed -n '1,25p' "$0" + exit 0 + fi + + if [[ $# -ge 1 ]]; then + local f="$1" + [[ -f "$f" ]] || die "file not found: $f" + + printf 'Deleting IDs from %s (one api at a time, infinite retries per api)\n' "$f" + + while IFS= read -r id || [[ -n "${id:-}" ]]; do + id="${id//$'\r'/}" + id="${id//[$'\t ']/}" + + [[ -z "$id" ]] && continue + printf 'next id: %s\n' "$id" + delete_one_until_ok "$id" + sleep "$sleep_between_apis_seconds" + done <"$f" + + return 0 + fi + + printf '%s querying ids: %s\n' "$(ts)" "$FILTER_QUERY" + + local ids + ids="$(list_ids_from_aws | tr '\t' '\n' | grep -v '^[[:space:]]*$' || true)" + [[ -n "$ids" ]] || printf '%s No matching REST APIs.\n' "$(ts)" + + while IFS= read -r id || [[ -n "${id:-}" ]]; do + id="${id//$'\r'/}" + id="${id//[$'\t ']/}" + + [[ -z "$id" ]] && continue + printf 'next id: %s\n' "$id" + delete_one_until_ok "$id" + sleep "$sleep_between_apis_seconds" + done <<<"$ids" +} + +main "$@" diff --git a/.repos/alchemy-effect/scripts/cleanup-apigateway-rest-apis.ts b/.repos/alchemy-effect/scripts/cleanup-apigateway-rest-apis.ts new file mode 100644 index 00000000000..e9f1779b197 --- /dev/null +++ b/.repos/alchemy-effect/scripts/cleanup-apigateway-rest-apis.ts @@ -0,0 +1,128 @@ +#!/usr/bin/env bun +/** + * Round-robin cleanup of leftover API Gateway REST APIs in the current + * account/region. Companion to {@link ./cleanup-apigateway-rest-apis.sh}, + * which iterates one API to completion before moving on; this version + * pulls IDs from a shared queue and re-queues anything throttled, so a + * single stubborn API never blocks the others. + * + * Usage: + * bun scripts/cleanup-apigateway-rest-apis.ts # name CONTAINS "Ag" + * FILTER_QUERY="..." bun scripts/cleanup-apigateway-rest-apis.ts + * SPACING_MS=35000 bun scripts/cleanup-apigateway-rest-apis.ts + * + * Environment: + * FILTER_QUERY JMESPath for `aws apigateway get-rest-apis --query` + * (default: items[?contains(name, `Ag`)].id) + * SPACING_MS Minimum delay between any two delete attempts. + * Default 35000 — AWS DeleteRestApi is 1 request per 30s + * account-wide, so we wait just over the window. + * AWS_PROFILE Standard AWS CLI variable. + * AWS_REGION Standard AWS CLI variable. + */ +import { execFileSync } from "node:child_process"; + +type Outcome = + | { kind: "deleted" } + | { kind: "throttled" } + | { kind: "error"; message: string }; + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +const ts = () => new Date().toISOString(); + +const FILTER_QUERY = + process.env.FILTER_QUERY ?? "items[?contains(name, `Ag`)].id"; +const SPACING_MS = Number(process.env.SPACING_MS ?? 35_000); + +const listApis = (): string[] => { + const out = execFileSync( + "aws", + [ + "apigateway", + "get-rest-apis", + "--query", + FILTER_QUERY, + "--output", + "text", + ], + { encoding: "utf8" }, + ); + return out + .split(/\s+/) + .map((s) => s.trim()) + .filter(Boolean); +}; + +const tryDelete = (id: string): Outcome => { + try { + execFileSync( + "aws", + ["apigateway", "delete-rest-api", "--rest-api-id", id], + { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + // Let this script own retry/backoff; SDK retries waste tokens. + AWS_MAX_ATTEMPTS: "1", + AWS_RETRY_MODE: "standard", + }, + }, + ); + return { kind: "deleted" }; + } catch (e: any) { + const msg = String(e.stderr ?? e.message ?? e); + if (msg.includes("TooManyRequests") || msg.includes("Throttling")) { + return { kind: "throttled" }; + } + if (msg.includes("NotFoundException") || msg.includes("does not exist")) { + return { kind: "deleted" }; + } + return { kind: "error", message: msg.split("\n").slice(0, 2).join(" ") }; + } +}; + +const main = async () => { + const ids = listApis(); + console.log( + `${ts()} found ${ids.length} REST APIs to delete (filter: ${FILTER_QUERY})`, + ); + + const queue = [...ids]; + let deleted = 0; + let lastAttemptAt = 0; + + while (queue.length > 0) { + const wait = SPACING_MS - (Date.now() - lastAttemptAt); + if (wait > 0) { + await sleep(wait); + } + const id = queue.shift()!; + lastAttemptAt = Date.now(); + const res = tryDelete(id); + if (res.kind === "deleted") { + deleted += 1; + console.log( + `${ts()} deleted ${id} (${deleted}/${ids.length}, ${queue.length} left)`, + ); + } else if (res.kind === "throttled") { + // Push to the back of the queue; the next attempt will go to a + // different ID. This drains the throttle bucket evenly across the + // remaining APIs instead of stalling on a single one. + queue.push(id); + console.error( + `${ts()} throttled ${id} — requeued (${queue.length} left)`, + ); + } else { + console.error(`${ts()} error ${id}: ${res.message}`); + } + } + + console.log(`${ts()} done — deleted ${deleted}/${ids.length}`); +}; + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/.repos/alchemy-effect/scripts/cleanup-cloudflare.ts b/.repos/alchemy-effect/scripts/cleanup-cloudflare.ts new file mode 100644 index 00000000000..3bd0b234c8e --- /dev/null +++ b/.repos/alchemy-effect/scripts/cleanup-cloudflare.ts @@ -0,0 +1,645 @@ +#!/usr/bin/env bun + +// @ts-nocheck +/** + * Bulk-delete Cloudflare resources in the active Alchemy profile's account. + * + * Cleans up: Workers, Hyperdrive configs, D1 databases, Queues, Workflows, + * R2 buckets. + * + * Workers and R2 buckets obey a name filter (alchemy-* are preserved by + * default). R2 buckets can't be deleted while they hold objects or custom + * domains, so each victim bucket is first detached from its custom domains + * and emptied of objects, then deleted. + * + * Workers + * that are queue consumers can't be deleted directly — Cloudflare returns + * `QueueConsumerConflict`. The script pre-builds a `scriptName → consumers` + * map by fanning `listConsumers` across every queue, and on conflict + * deletes each consumer first then retries the script delete. + * + * Hyperdrive, D1, Queues, and Workflows are deleted unconditionally + * (the API enforces unique names within the account; nothing to keep). + * + * Authentication resolves through the active Alchemy profile + * (`ALCHEMY_PROFILE`, default `default`). + * + * Usage: + * bun scripts/cleanup-cloudflare.ts + * KEEP=alchemy- DELETE_MATCH=pr-,distilled bun scripts/cleanup-cloudflare.ts + * DRY_RUN=1 bun scripts/cleanup-cloudflare.ts + * CONCURRENCY=16 bun scripts/cleanup-cloudflare.ts + * SKIP=hyperdrive,d1 bun scripts/cleanup-cloudflare.ts + * ALCHEMY_PROFILE=staging bun scripts/cleanup-cloudflare.ts + */ +import { + Credentials as CfCredentials, + formatHeaders, +} from "@distilled.cloud/cloudflare/Credentials"; +import * as d1 from "@distilled.cloud/cloudflare/d1"; +import * as hyperdrive from "@distilled.cloud/cloudflare/hyperdrive"; +import * as queues from "@distilled.cloud/cloudflare/queues"; +import * as r2 from "@distilled.cloud/cloudflare/r2"; +import * as workers from "@distilled.cloud/cloudflare/workers"; +import * as workflows from "@distilled.cloud/cloudflare/workflows"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Stream from "effect/Stream"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import { AuthProviders } from "../packages/alchemy/src/Auth/AuthProvider.ts"; +import { CredentialsStoreLive } from "../packages/alchemy/src/Auth/Credentials.ts"; +import { ProfileLive } from "../packages/alchemy/src/Auth/Profile.ts"; +import { CloudflareAuth } from "../packages/alchemy/src/Cloudflare/Auth/AuthProvider.ts"; +import { + CloudflareEnvironment, + fromProfile, +} from "../packages/alchemy/src/Cloudflare/CloudflareEnvironment.ts"; +import { fromAuthProvider } from "../packages/alchemy/src/Cloudflare/Credentials.ts"; +import { + PlatformServices, + runMain, +} from "../packages/alchemy/src/Util/PlatformServices.ts"; + +const KEEP = (process.env.KEEP ?? "alchemy-").toLowerCase(); +const DELETE_PATTERNS = (process.env.DELETE_MATCH ?? "") + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter((s) => s.length > 0); +const DRY_RUN = process.env.DRY_RUN === "1"; +const CONCURRENCY = Math.max(1, Number(process.env.CONCURRENCY ?? 8)); +const SKIP = new Set( + (process.env.SKIP ?? "") + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter((s) => s.length > 0), +); + +const shouldDeleteName = (name: string): boolean => { + const lower = name.toLowerCase(); + if (DELETE_PATTERNS.some((p) => lower.includes(p))) return true; + if (!lower.includes(KEEP)) return true; + return false; +}; + +/** + * Walk every queue page in the account. The distilled SDK's `listQueues` + * only returns the first page (100 results); large accounts overflow. + */ +const listAllQueueIds = (accountId: string) => + Effect.gen(function* () { + const credentialsEff = yield* CfCredentials; + const credentials = yield* credentialsEff; + const headers = formatHeaders(credentials); + const client = yield* HttpClient.HttpClient; + + const ids: { queueId: string; queueName: string }[] = []; + for (let page = 1; ; page++) { + const res = yield* client.execute( + HttpClientRequest.get( + `${credentials.apiBaseUrl}/accounts/${accountId}/queues?page=${page}`, + ).pipe(HttpClientRequest.setHeaders(headers)), + ); + const body = (yield* res.json) as { + result?: { queue_id?: string; queue_name?: string }[] | null; + }; + const batch = body.result ?? []; + if (batch.length === 0) break; + for (const q of batch) { + if (q.queue_id) { + ids.push({ + queueId: q.queue_id, + queueName: q.queue_name ?? q.queue_id, + }); + } + } + } + return ids; + }); + +/** + * Map `scriptName → [{queueId, consumerId}, ...]` built by fanning out + * `listConsumers` across every queue in the account. + * + * Used to clear queue-consumer bindings before re-trying a worker delete + * that failed with `QueueConsumerConflict`. + */ +const buildConsumerMap = (accountId: string, queueIds: ReadonlyArray) => + Effect.gen(function* () { + const map = new Map(); + yield* Effect.forEach( + queueIds, + (queueId) => + queues.listConsumers({ accountId, queueId }).pipe( + Effect.tap((res) => + Effect.sync(() => { + for (const c of res.result ?? []) { + const consumerId = c.consumerId; + // The distilled SDK exposes the consumer's worker as + // `scriptName` (from `script_name`), not `script`. + const script = c.scriptName ?? undefined; + if (!consumerId || !script) continue; + const entry = map.get(script) ?? []; + entry.push({ queueId, consumerId }); + map.set(script, entry); + } + }), + ), + Effect.catch(() => Effect.void), + ), + { concurrency: 8, discard: true }, + ); + return map; + }); + +/** + * Last-resort cleanup: PUT a minimal script over the worker with no + * bindings, no queue consumer config. Cloudflare drops the queue-consumer + * relationship when the script's bindings list no longer contains it, so + * a follow-up `deleteScript` then succeeds. Used when `listQueues` has + * no matching consumer entry but the API still insists the worker is a + * consumer (orphaned reference from a deleted queue). + */ +const forceOverwriteAndDelete = (accountId: string, scriptName: string) => + Effect.gen(function* () { + const credentialsEff = yield* CfCredentials; + const credentials = yield* credentialsEff; + const headers = formatHeaders(credentials); + + const form = new FormData(); + form.append( + "metadata", + JSON.stringify({ main_module: "worker.js", bindings: [] }), + ); + form.append( + "worker.js", + new Blob( + [ + "export default { fetch() { return new Response('deleted', { status: 410 }); } };", + ], + { type: "application/javascript+module" }, + ), + "worker.js", + ); + + const client = yield* HttpClient.HttpClient; + yield* client.execute( + HttpClientRequest.put( + `${credentials.apiBaseUrl}/accounts/${accountId}/workers/scripts/${encodeURIComponent(scriptName)}`, + ).pipe( + HttpClientRequest.setHeaders(headers), + HttpClientRequest.bodyFormData(form), + ), + ); + yield* Console.log(` ↳ overwrote ${scriptName} with empty script`); + return yield* workers.deleteScript({ + accountId, + scriptName, + force: true, + }); + }); + +const deleteWorkerWithConsumerRecovery = ( + accountId: string, + scriptName: string, + consumers: Map, +) => + workers.deleteScript({ accountId, scriptName, force: true }).pipe( + Effect.catchTag("QueueConsumerConflict", () => + Effect.gen(function* () { + const bindings = consumers.get(scriptName) ?? []; + if (bindings.length === 0) { + return yield* forceOverwriteAndDelete(accountId, scriptName); + } + for (const { queueId, consumerId } of bindings) { + yield* queues.deleteConsumer({ accountId, queueId, consumerId }).pipe( + Effect.tap(() => + Console.log( + ` ↳ unbound consumer ${consumerId} from queue ${queueId} for ${scriptName}`, + ), + ), + Effect.catchTag("ConsumerNotFound", () => Effect.void), + ); + } + return yield* workers + .deleteScript({ accountId, scriptName, force: true }) + .pipe( + Effect.catchTag("QueueConsumerConflict", () => + forceOverwriteAndDelete(accountId, scriptName), + ), + ); + }), + ), + ); + +interface CleanupResult { + readonly kind: string; + readonly listed: number; + readonly ok: number; + readonly fail: number; +} + +const driveCleanup = ( + kind: string, + items: ReadonlyArray, + label: (item: Item) => string, + del: (item: Item) => Effect.Effect, +): Effect.Effect => + Effect.gen(function* () { + yield* Console.log(`\n=== ${kind} ===`); + yield* Console.log(`→ found ${items.length}`); + for (const item of items) yield* Console.log(` - ${label(item)}`); + if (DRY_RUN || items.length === 0) { + return { kind, listed: items.length, ok: 0, fail: 0 }; + } + let ok = 0; + let fail = 0; + yield* Effect.forEach( + items, + (item) => + del(item).pipe( + Effect.tap(() => + Effect.sync(() => { + ok += 1; + console.log( + `✓ ${kind}: deleted ${label(item)} (${ok}/${items.length})`, + ); + }), + ), + Effect.catch((e) => + Effect.sync(() => { + fail += 1; + console.error(`✗ ${kind}: ${label(item)}: ${String(e)}`); + }), + ), + ), + { concurrency: CONCURRENCY, discard: true }, + ); + return { kind, listed: items.length, ok, fail }; + }); + +const cleanupWorkers = (accountId: string, queueIds: ReadonlyArray) => + Effect.gen(function* () { + const consumerMap = yield* buildConsumerMap(accountId, queueIds); + yield* Console.log( + `→ queue-consumer bindings indexed for ${consumerMap.size} scripts`, + ); + const all = yield* workers.listScripts.items({ accountId }).pipe( + Stream.runCollect, + Effect.map((chunk) => + Array.from(chunk).flatMap((s): string[] => + s.id == null ? [] : [s.id], + ), + ), + ); + yield* Console.log(`→ total worker scripts: ${all.length}`); + const victims = all.filter(shouldDeleteName); + yield* Console.log( + `→ workers to delete: ${victims.length} (keep=${JSON.stringify(KEEP)} deleteMatch=${JSON.stringify(DELETE_PATTERNS)})`, + ); + return yield* driveCleanup( + "workers", + victims, + (name) => name, + (name) => deleteWorkerWithConsumerRecovery(accountId, name, consumerMap), + ); + }); + +const cleanupHyperdrives = (accountId: string) => + Effect.gen(function* () { + const all = yield* hyperdrive.listConfigs.items({ accountId }).pipe( + Stream.runCollect, + Effect.map((chunk) => Array.from(chunk)), + ); + return yield* driveCleanup( + "hyperdrive", + all, + (c) => `${c.name} (${c.id})`, + (c) => + hyperdrive + .deleteConfig({ accountId, hyperdriveId: c.id }) + .pipe(Effect.catchTag("HyperdriveConfigNotFound", () => Effect.void)), + ); + }); + +const cleanupD1 = (accountId: string) => + Effect.gen(function* () { + const all = yield* d1.listDatabases.items({ accountId }).pipe( + Stream.runCollect, + Effect.map((chunk) => + Array.from(chunk).flatMap((db): { uuid: string; name: string }[] => + db.uuid == null ? [] : [{ uuid: db.uuid, name: db.name ?? db.uuid }], + ), + ), + ); + return yield* driveCleanup( + "d1", + all, + (db) => `${db.name} (${db.uuid})`, + (db) => + d1 + .deleteDatabase({ accountId, databaseId: db.uuid }) + .pipe(Effect.catchTag("DatabaseNotFound", () => Effect.void)), + ); + }); + +const cleanupQueues = ( + accountId: string, + queueIds: ReadonlyArray<{ queueId: string; queueName: string }>, +) => + driveCleanup( + "queues", + queueIds, + (q) => `${q.queueName} (${q.queueId})`, + (q) => + queues + .deleteQueue({ accountId, queueId: q.queueId }) + .pipe(Effect.catchTag("QueueNotFound", () => Effect.void)), + ); + +// The distilled SDK's `listWorkflows` schema requires `className: string`, +// but Cloudflare returns `class_name: null` for some workflows. Use raw +// HTTP to side-step the schema decoder. +const listAllWorkflows = (accountId: string) => + Effect.gen(function* () { + const credentialsEff = yield* CfCredentials; + const credentials = yield* credentialsEff; + const headers = formatHeaders(credentials); + const client = yield* HttpClient.HttpClient; + + const items: { id: string; name: string }[] = []; + for (let page = 1; ; page++) { + const res = yield* client.execute( + HttpClientRequest.get( + `${credentials.apiBaseUrl}/accounts/${accountId}/workflows?page=${page}&per_page=100`, + ).pipe(HttpClientRequest.setHeaders(headers)), + ); + const body = (yield* res.json) as { + result?: { id?: string; name?: string }[] | null; + }; + const batch = body.result ?? []; + if (batch.length === 0) break; + for (const w of batch) { + if (w.id && w.name) items.push({ id: w.id, name: w.name }); + } + if (batch.length < 100) break; + } + return items; + }); + +const cleanupWorkflows = (accountId: string) => + Effect.gen(function* () { + const all = yield* listAllWorkflows(accountId); + // `DELETE /workflows/{name}` 400s with `no_deployed_versions` for any + // workflow that never had a version deployed — the control plane can't + // delete a versionless workflow. Issuing that DELETE is the bad request, + // so filter those out up front (check the first page of versions) instead + // of firing a doomed delete and swallowing the error. + const deletable = yield* Effect.forEach( + all, + (w) => + workflows.listVersions({ accountId, workflowName: w.name }).pipe( + Effect.map((res) => ((res.result?.length ?? 0) > 0 ? [w] : [])), + Effect.catchTag("WorkflowNotFound", () => Effect.succeed([])), + ), + { concurrency: 8 }, + ).pipe(Effect.map((xs) => xs.flat())); + const skipped = all.length - deletable.length; + if (skipped > 0) { + yield* Console.log( + `→ skipping ${skipped} workflow(s) with no deployed versions (not deletable)`, + ); + } + return yield* driveCleanup( + "workflows", + deletable, + (w) => `${w.name} (${w.id})`, + (w) => + workflows + .deleteWorkflow({ accountId, workflowName: w.name }) + .pipe(Effect.catchTag("WorkflowNotFound", () => Effect.void)), + ); + }); + +type R2Jurisdiction = "default" | "eu" | "fedramp"; + +interface R2BucketRecord { + readonly name: string; + readonly jurisdiction: R2Jurisdiction; +} + +/** + * Walk every R2 bucket in the account. `listBuckets` is cursor-paginated by + * bucket name; the distilled SDK exposes the raw method, so page manually with + * `startAfter` (lexicographic) until a short page signals the end. + */ +const listAllBuckets = (accountId: string) => + Effect.gen(function* () { + const out: R2BucketRecord[] = []; + let startAfter: string | undefined; + for (;;) { + const res = yield* r2.listBuckets({ + accountId, + perPage: 1000, + startAfter, + }); + const batch = res.buckets ?? []; + for (const b of batch) { + if (b?.name) { + out.push({ + name: b.name, + jurisdiction: (b.jurisdiction ?? "default") as R2Jurisdiction, + }); + } + } + if (batch.length < 1000) break; + startAfter = batch[batch.length - 1]?.name ?? undefined; + if (!startAfter) break; + } + return out; + }); + +/** + * R2 refuses to delete a bucket that still has custom domains, event + * notification configurations, or objects. Detach every custom domain, remove + * every per-queue notification config, drain all objects in 1000-key batches, + * then delete the bucket. Each step tolerates a vanished bucket/config so the + * cleanup is idempotent. + */ +const emptyAndDeleteBucket = (accountId: string, bucket: R2BucketRecord) => + Effect.gen(function* () { + const { name: bucketName, jurisdiction } = bucket; + + const domains = yield* r2 + .listBucketDomainCustoms({ accountId, bucketName, jurisdiction }) + .pipe( + Effect.map((res) => res.domains ?? []), + Effect.catchTag("NoSuchBucket", () => Effect.succeed([])), + ); + yield* Effect.forEach( + domains, + (d) => + r2 + .deleteBucketDomainCustom({ + accountId, + bucketName, + domain: d.domain, + jurisdiction, + }) + .pipe( + Effect.tap(() => + Console.log(` ↳ detached domain ${d.domain} from ${bucketName}`), + ), + Effect.ignore, + ), + { concurrency: 4, discard: true }, + ); + + const notifications = yield* r2 + .listBucketEventNotifications({ accountId, bucketName, jurisdiction }) + .pipe( + Effect.map((res) => + (res.queues ?? []).flatMap((q): string[] => + q.queueId ? [q.queueId] : [], + ), + ), + Effect.catchTag("NoSuchBucket", () => Effect.succeed([])), + Effect.catchTag("BucketNotFound", () => Effect.succeed([])), + Effect.catchTag("NoEventNotificationConfig", () => Effect.succeed([])), + ); + yield* Effect.forEach( + notifications, + (queueId) => + r2 + .deleteBucketEventNotification({ + accountId, + bucketName, + queueId, + jurisdiction, + }) + .pipe( + Effect.tap(() => + Console.log( + ` ↳ removed notification config (queue ${queueId}) from ${bucketName}`, + ), + ), + Effect.ignore, + ), + { concurrency: 4, discard: true }, + ); + + yield* r2.listObjects + .items({ + accountId, + bucketName, + cfR2Jurisdiction: jurisdiction, + perPage: 1000, + }) + .pipe( + Stream.filter( + (o): o is typeof o & { key: string } => + typeof o.key === "string" && o.key !== "", + ), + Stream.map((o) => o.key), + Stream.runForEachArray((chunk) => + r2.deleteObjects({ + accountId, + bucketName, + cfR2Jurisdiction: jurisdiction, + body: [...chunk], + }), + ), + Effect.catchTag("NoSuchBucket", () => Effect.void), + ); + + return yield* r2 + .deleteBucket({ accountId, bucketName, jurisdiction }) + .pipe(Effect.catchTag("NoSuchBucket", () => Effect.void)); + }); + +const cleanupR2 = (accountId: string) => + Effect.gen(function* () { + const all = yield* listAllBuckets(accountId); + yield* Console.log(`→ total R2 buckets: ${all.length}`); + const victims = all.filter((b) => shouldDeleteName(b.name)); + yield* Console.log( + `→ R2 buckets to delete: ${victims.length} (keep=${JSON.stringify(KEEP)} deleteMatch=${JSON.stringify(DELETE_PATTERNS)})`, + ); + return yield* driveCleanup( + "r2", + victims, + (b) => `${b.name} (${b.jurisdiction})`, + (b) => emptyAndDeleteBucket(accountId, b), + ); + }); + +const program = Effect.gen(function* () { + const { accountId } = yield* CloudflareEnvironment; + yield* Console.log( + `→ account=${accountId} dryRun=${DRY_RUN} concurrency=${CONCURRENCY} skip=${JSON.stringify([...SKIP])}`, + ); + + const queueRecords = + SKIP.has("queues") && SKIP.has("workers") + ? [] + : yield* listAllQueueIds(accountId); + const queueIds = queueRecords.map((q) => q.queueId); + + const results: CleanupResult[] = []; + + // Workflows first — they may reference workers. + if (!SKIP.has("workflows")) { + results.push(yield* cleanupWorkflows(accountId)); + } + // Hyperdrive next — independent. + if (!SKIP.has("hyperdrive")) { + results.push(yield* cleanupHyperdrives(accountId)); + } + // Workers — clears queue-consumer bindings, may PUT empty scripts. + if (!SKIP.has("workers")) { + results.push(yield* cleanupWorkers(accountId, queueIds)); + } + // Queues — after workers so producers/consumers are gone. + if (!SKIP.has("queues")) { + results.push(yield* cleanupQueues(accountId, queueRecords)); + } + // D1 — workers using it should be gone first. + if (!SKIP.has("d1")) { + results.push(yield* cleanupD1(accountId)); + } + // R2 — independent of the above; detaches domains + empties before delete. + if (!SKIP.has("r2")) { + results.push(yield* cleanupR2(accountId)); + } + + yield* Console.log("\n=== summary ==="); + for (const r of results) { + yield* Console.log( + ` ${r.kind}: listed=${r.listed} deleted=${r.ok} failed=${r.fail}`, + ); + } + const totalFail = results.reduce((acc, r) => acc + r.fail, 0); + if (totalFail > 0) { + return yield* Effect.fail(new Error(`${totalFail} delete(s) failed`)); + } +}); + +const authProviders: AuthProviders["Service"] = {}; +const authRegistry = Layer.succeed(AuthProviders, authProviders); +const authLayer = Layer.provideMerge(CloudflareAuth, authRegistry); + +const profile = Layer.mergeAll( + Layer.provide(ProfileLive, PlatformServices), + Layer.provide(CredentialsStoreLive, PlatformServices), +); + +const cloudflare = Layer.mergeAll(fromAuthProvider(), fromProfile()).pipe( + Layer.provide(authLayer), + Layer.provide(profile), +); + +const services = Layer.mergeAll(cloudflare, FetchHttpClient.layer); + +runMain(program.pipe(Effect.provide(services))); diff --git a/.repos/alchemy-effect/scripts/clone-external b/.repos/alchemy-effect/scripts/clone-external new file mode 100755 index 00000000000..21c7a1a905d --- /dev/null +++ b/.repos/alchemy-effect/scripts/clone-external @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +mkdir -p .external +cd .external +if [ ! -d "terraform-provider-aws" ]; then + git clone git@github.com:hashicorp/terraform-provider-aws.git --depth=1 +fi \ No newline at end of file diff --git a/.repos/alchemy-effect/scripts/generate-api-reference.ts b/.repos/alchemy-effect/scripts/generate-api-reference.ts new file mode 100644 index 00000000000..09954b84f29 --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-api-reference.ts @@ -0,0 +1,546 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; + +import { + Node, + Project, + SyntaxKind, + type JSDoc, + type SourceFile, +} from "ts-morph"; + +const websiteRoot = path.join(import.meta.dir, "../website"); + +const config = { + srcRoot: path.join(import.meta.dir, "../packages/alchemy/src"), + outRoot: path.join(websiteRoot, "src/content/docs/providers"), + tsConfig: path.join(import.meta.dir, "../packages/alchemy/tsconfig.json"), + excludeFile(baseName: string): boolean { + if (baseName === "index.ts") return true; + if (/^[a-z]/.test(baseName)) return true; + return false; + }, +}; + +interface FileEntry { + relativePath: string; + absolutePath: string; + outputPath: string; +} + +interface ExampleBlock { + title: string; + body: string; +} + +interface ExampleSection { + title: string; + description: string; + examples: ExampleBlock[]; +} + +interface PageDoc { + title: string; + relativePath: string; + summary: string; + sections: ExampleSection[]; +} + +const normalizeSlashes = (value: string) => value.split(path.sep).join("/"); + +async function discoverFiles(): Promise { + const entries: FileEntry[] = []; + + const topLevelEntries = await fs.readdir(config.srcRoot, { + withFileTypes: true, + }); + const dirs = topLevelEntries + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); + + for (const dir of dirs) { + const dirPath = path.join(config.srcRoot, dir); + let files: string[]; + try { + files = (await fs.readdir(dirPath, { recursive: true })) as string[]; + } catch { + continue; + } + + for (const file of files) { + const baseName = path.basename(file); + if (!baseName.endsWith(".ts") && !baseName.endsWith(".tsx")) continue; + if (baseName.endsWith(".d.ts")) continue; + if (config.excludeFile(baseName)) continue; + + const relativePath = path.join(dir, file); + const parts = normalizeSlashes(relativePath).split("/"); + const cloud = parts[0]; + const service = parts.length >= 3 ? parts[1] : undefined; + const resource = path.basename(file, path.extname(file)); + + let outputRelative: string; + if (cloud === "Cloudflare") { + outputRelative = path.join(cloud, `${resource}.md`); + } else if (service) { + outputRelative = path.join(cloud, service, `${resource}.md`); + } else { + outputRelative = path.join(cloud, `${resource}.md`); + } + + entries.push({ + relativePath, + absolutePath: path.join(config.srcRoot, relativePath), + outputPath: path.join(config.outRoot, outputRelative), + }); + } + } + + entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); + return entries; +} + +function getJsDocBlocks(node: Node): JSDoc[] { + const getter = (node as Node & { getJsDocs?: () => JSDoc[] }).getJsDocs; + return getter ? getter.call(node) : []; +} + +function cleanDocComment(raw: string): string { + return raw + .replace(/^\/\*\*?/, "") + .replace(/\*\/$/, "") + .split("\n") + .map((line) => line.replace(/^\s*\*\s?/, "")) + .join("\n"); +} + +interface ParsedJSDoc { + summary: string; + sections: ExampleSection[]; + hasResourceTag: boolean; + hasBindingTag: boolean; +} + +function parseJSDoc(node: Node): ParsedJSDoc { + const docs = getJsDocBlocks(node); + if (docs.length === 0) { + return { + summary: "", + sections: [], + hasResourceTag: false, + hasBindingTag: false, + }; + } + + const clean = cleanDocComment(docs.map((doc) => doc.getText()).join("\n")); + const lines = clean.split("\n"); + + const summaryLines: string[] = []; + const sections: ExampleSection[] = []; + let hasResourceTag = false; + let hasBindingTag = false; + let sawTag = false; + let currentSection: ExampleSection | undefined; + let currentExample: ExampleBlock | undefined; + + let sectionDescLines: string[] = []; + let collectingSectionDesc = false; + + const flushExample = () => { + if (!currentExample) return; + currentExample.body = currentExample.body.trim(); + if (!currentSection) { + currentSection = { title: "Examples", description: "", examples: [] }; + sections.push(currentSection); + } + currentSection.examples.push(currentExample); + currentExample = undefined; + }; + + const flushSectionDesc = () => { + if (currentSection && sectionDescLines.length > 0) { + currentSection.description = sectionDescLines.join("\n").trim(); + } + sectionDescLines = []; + collectingSectionDesc = false; + }; + + for (const line of lines) { + const tag = line.trimEnd().match(/^@(\w+)\s*(.*)$/); + if (tag) { + sawTag = true; + const [, name, rest] = tag; + const value = (rest ?? "").trim(); + switch (name) { + case "resource": + hasResourceTag = true; + break; + case "binding": + hasBindingTag = true; + break; + case "section": + flushExample(); + flushSectionDesc(); + currentSection = { + title: value || "Examples", + description: "", + examples: [], + }; + sections.push(currentSection); + collectingSectionDesc = true; + break; + case "example": + flushSectionDesc(); + flushExample(); + currentExample = { title: value || "Example", body: "" }; + break; + } + continue; + } + + if (!sawTag) { + summaryLines.push(line); + continue; + } + + if (currentExample) { + currentExample.body += `${line}\n`; + } else if (collectingSectionDesc) { + sectionDescLines.push(line); + } + } + + flushSectionDesc(); + flushExample(); + + return { + summary: summaryLines.join("\n").trim(), + sections, + hasResourceTag, + hasBindingTag, + }; +} + +function findPrimaryJSDoc(sourceFile: SourceFile): ParsedJSDoc { + for (const decl of sourceFile.getVariableDeclarations()) { + if (!decl.isExported()) continue; + const init = decl.getInitializerIfKind(SyntaxKind.CallExpression); + const expr = init?.getExpression().getText(); + if (expr === "Resource" || expr === "Host" || expr === "Platform") { + const stmt = decl.getVariableStatement(); + if (stmt) { + const jsdoc = parseJSDoc(stmt); + if (jsdoc.summary || jsdoc.sections.length > 0 || jsdoc.hasResourceTag) + return jsdoc; + } + } + } + + for (const cls of sourceFile.getClasses()) { + if (!cls.isExported()) continue; + const jsdoc = parseJSDoc(cls); + if ( + jsdoc.hasResourceTag || + jsdoc.hasBindingTag || + jsdoc.sections.length > 0 + ) + return jsdoc; + if (jsdoc.summary) return jsdoc; + } + + let firstWithSummary: ParsedJSDoc | undefined; + for (const stmt of sourceFile.getStatements()) { + if (Node.isExportable(stmt) && stmt.isExported()) { + const jsdoc = parseJSDoc(stmt); + if ( + jsdoc.hasResourceTag || + jsdoc.hasBindingTag || + jsdoc.sections.length > 0 + ) + return jsdoc; + if (!firstWithSummary && jsdoc.summary) firstWithSummary = jsdoc; + } + } + + if (firstWithSummary) return firstWithSummary; + + const rawJSDocBlocks = sourceFile.getFullText().match(/\/\*\*[\s\S]*?\*\//g); + if (rawJSDocBlocks) { + for (const block of rawJSDocBlocks) { + if ( + block.includes("@section") || + block.includes("@resource") || + block.includes("@binding") + ) { + const clean = cleanDocComment(block); + const lines = clean.split("\n"); + const summaryLines: string[] = []; + const sections: ExampleSection[] = []; + let hasResourceTag = false; + let hasBindingTag = false; + let sawTag = false; + let currentSection: ExampleSection | undefined; + let currentExample: ExampleBlock | undefined; + let sectionDescLines: string[] = []; + let collectingSectionDesc = false; + + const flushExample = () => { + if (!currentExample) return; + currentExample.body = currentExample.body.trim(); + if (!currentSection) { + currentSection = { + title: "Examples", + description: "", + examples: [], + }; + sections.push(currentSection); + } + currentSection.examples.push(currentExample); + currentExample = undefined; + }; + + const flushSectionDesc = () => { + if (currentSection && sectionDescLines.length > 0) { + currentSection.description = sectionDescLines.join("\n").trim(); + } + sectionDescLines = []; + collectingSectionDesc = false; + }; + + for (const line of lines) { + const tag = line.trimEnd().match(/^@(\w+)\s*(.*)$/); + if (tag) { + sawTag = true; + const [, name, rest] = tag; + const value = (rest ?? "").trim(); + switch (name) { + case "resource": + hasResourceTag = true; + break; + case "binding": + hasBindingTag = true; + break; + case "section": + flushExample(); + flushSectionDesc(); + currentSection = { + title: value || "Examples", + description: "", + examples: [], + }; + sections.push(currentSection); + collectingSectionDesc = true; + break; + case "example": + flushSectionDesc(); + flushExample(); + currentExample = { title: value || "Example", body: "" }; + break; + } + continue; + } + if (!sawTag) { + summaryLines.push(line); + continue; + } + if (currentExample) { + currentExample.body += `${line}\n`; + } else if (collectingSectionDesc) { + sectionDescLines.push(line); + } + } + flushSectionDesc(); + flushExample(); + + const summary = summaryLines.join("\n").trim(); + if (summary || sections.length > 0) { + return { summary, sections, hasResourceTag, hasBindingTag }; + } + } + } + } + + return { + summary: "", + sections: [], + hasResourceTag: false, + hasBindingTag: false, + }; +} + +function isResourceFile(sourceFile: SourceFile): boolean { + const fullText = sourceFile.getFullText(); + + if (/^\s*\*\s*@internal\s*$/m.test(fullText)) { + const blocks = fullText.match(/\/\*\*[\s\S]*?\*\//g) || []; + for (const block of blocks) { + if (!/^\s*\*\s*@internal\s*$/m.test(block)) continue; + const end = fullText.indexOf(block) + block.length; + const after = fullText.slice(end).trimStart(); + if (after.startsWith("export ")) return false; + } + } + + if (fullText.includes("@resource") || fullText.includes("@binding")) + return true; + + const text = sourceFile.getFullText(); + if (text.includes("Binding.Service<") || text.includes("Binding.Policy<")) { + if ( + !text.includes("= Resource<") && + !text.includes("extends Resource<") && + !text.includes("= Host<") && + !text.includes("extends Host<") && + !text.includes("Platform(") + ) { + return false; + } + } + + for (const decl of sourceFile.getVariableDeclarations()) { + if (!decl.isExported()) continue; + const init = decl.getInitializerIfKind(SyntaxKind.CallExpression); + if (!init) continue; + const expr = init.getExpression().getText(); + if (expr === "Resource" || expr === "Host" || expr === "Platform") + return true; + const innerCall = init.getExpression(); + if (Node.isCallExpression(innerCall)) { + const innerExpr = innerCall.getExpression().getText(); + if (innerExpr === "Resource" || innerExpr === "Host") return true; + } + } + for (const iface of sourceFile.getInterfaces()) { + if (!iface.isExported()) continue; + const hasResourceHeritage = iface + .getHeritageClauses() + .flatMap((clause) => clause.getTypeNodes()) + .some((typeNode) => { + const expr = typeNode.getExpression().getText(); + return expr === "Resource" || expr === "Host"; + }); + if (hasResourceHeritage) return true; + } + for (const typeAlias of sourceFile.getTypeAliases()) { + if (!typeAlias.isExported()) continue; + const typeText = typeAlias.getTypeNode()?.getText() ?? ""; + if (typeText.startsWith("Resource<") || typeText.startsWith("Host<")) { + return true; + } + } + return false; +} + +function parseFile(sourceFile: SourceFile, relativePath: string): PageDoc { + const baseName = path.basename(relativePath, path.extname(relativePath)); + const primary = findPrimaryJSDoc(sourceFile); + + return { + title: baseName, + relativePath, + summary: primary.summary, + sections: primary.sections, + }; +} + +function yamlString(value: string): string { + if (/[\n:"{}[\],&*?|>!%@`#]/.test(value) || value.trim() !== value) { + return JSON.stringify(value); + } + return value; +} + +function firstParagraph(value: string): string { + const idx = value.indexOf("\n\n"); + const para = idx === -1 ? value : value.slice(0, idx); + return para.replace(/\s+/g, " ").trim(); +} + +function renderPageBody(doc: PageDoc): string { + const parts: string[] = []; + + if (doc.summary) { + parts.push(doc.summary); + } + + for (const section of doc.sections) { + const secParts = [`## ${section.title}`]; + if (section.description) { + secParts.push(section.description); + } + for (const example of section.examples) { + if (section.examples.length > 1) { + secParts.push(`**${example.title}**`); + } + secParts.push(example.body); + } + parts.push(secParts.join("\n\n")); + } + + return parts.join("\n\n"); +} + +function renderPage(doc: PageDoc): string { + const sourcePath = `src/${normalizeSlashes(doc.relativePath)}`; + const description = + firstParagraph(doc.summary) || `API reference for ${doc.title}`; + const frontmatter = [ + "---", + `title: ${yamlString(doc.title)}`, + `description: ${yamlString(description)}`, + "---", + ].join("\n"); + + const sourceBlock = `> **Source:** \`${sourcePath}\``; + const body = renderPageBody(doc).trim(); + + if (body) { + return `${frontmatter}\n\n${sourceBlock}\n\n${body}\n`; + } + return `${frontmatter}\n\n${sourceBlock}\n`; +} + +async function main() { + const entries = await discoverFiles(); + console.log(`Discovered ${entries.length} source files.`); + + const project = new Project({ + tsConfigFilePath: config.tsConfig, + skipFileDependencyResolution: true, + }); + + await fs.rm(config.outRoot, { recursive: true, force: true }); + await fs.mkdir(config.outRoot, { recursive: true }); + + const resourceEntries: FileEntry[] = []; + for (const entry of entries) { + const sourceFile = project.getSourceFile(entry.absolutePath); + if (!sourceFile) { + console.warn(` skipped: ${entry.relativePath}`); + continue; + } + if (!isResourceFile(sourceFile)) continue; + resourceEntries.push(entry); + } + + console.log( + `Filtered to ${resourceEntries.length} documented files (excluded ${entries.length - resourceEntries.length} unannotated files).`, + ); + + let written = 0; + for (const entry of resourceEntries) { + const sourceFile = project.getSourceFile(entry.absolutePath)!; + const doc = parseFile(sourceFile, entry.relativePath); + + await fs.mkdir(path.dirname(entry.outputPath), { recursive: true }); + await fs.writeFile(entry.outputPath, renderPage(doc), "utf8"); + + written++; + } + + console.log( + `Done. Wrote ${written} resource pages to ${normalizeSlashes(path.relative(path.join(import.meta.dir, ".."), config.outRoot))}.`, + ); +} + +await main(); diff --git a/.repos/alchemy-effect/scripts/generate-cfn-docs.ts b/.repos/alchemy-effect/scripts/generate-cfn-docs.ts new file mode 100644 index 00000000000..9f2a5a61e23 --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-cfn-docs.ts @@ -0,0 +1,869 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +const CONCURRENCY = 10; +const MAX_RETRIES = 3; +const INITIAL_BACKOFF_MS = 1000; + +let failedRequests = 0; + +const CFN_SPEC_URL = + "https://d1uauaxba7bl26.cloudfront.net/latest/gzip/CloudFormationResourceSpecification.json"; + +interface Property { + Documentation?: string; + UpdateType?: "Mutable" | "Immutable" | "Conditional"; + Required?: boolean; + Type?: string; + PrimitiveType?: string; + ItemType?: string; + PrimitiveItemType?: string; + DuplicatesAllowed?: boolean; +} + +interface Attribute { + Type?: string; + PrimitiveType?: string; + ItemType?: string; + PrimitiveItemType?: string; +} + +interface ResourceType { + Documentation?: string; + Properties?: Record; + Attributes?: Record; +} + +interface PropertyType { + Documentation?: string; + Properties?: Record; +} + +interface CFNSpec { + ResourceSpecificationVersion: string; + ResourceTypes: Record; + PropertyTypes: Record; +} + +// Scraped data from HTML +interface ScrapedResourceData { + description: string; + propertyDescriptions: Map; + returnValues: ReturnValueInfo[]; + examples: Example[]; +} + +interface PropertyDescription { + description: string; + allowedValues?: string[]; +} + +interface ReturnValueInfo { + name: string; + description: string; + example?: string; +} + +interface Example { + title: string; + description: string; + json?: string; + yaml?: string; +} + +// Parse AWS::Service::Resource into { service, resource } +function parseResourceType( + type: string, +): { service: string; resource: string } | null { + const parts = type.split("::"); + if (parts.length !== 3 || parts[0] !== "AWS") return null; + return { service: parts[1], resource: parts[2] }; +} + +// Parse AWS::Service::Resource.PropertyType into { service, resource, propertyType } +function parsePropertyType( + type: string, +): { service: string; resource: string; propertyType: string } | null { + const match = type.match(/^AWS::([^:]+)::([^.]+)\.(.+)$/); + if (!match) return null; + return { service: match[1], resource: match[2], propertyType: match[3] }; +} + +// Convert service name to lowercase kebab-case for directory naming +function toKebabCase(str: string): string { + return str + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2") + .toLowerCase(); +} + +// Format a property type for display (no links, just type names for LLM consumption) +function formatType(prop: Property | Attribute): string { + if (prop.PrimitiveType) { + return prop.PrimitiveType; + } + if (prop.Type === "List") { + if (prop.PrimitiveItemType) { + return `List<${prop.PrimitiveItemType}>`; + } + if (prop.ItemType) { + return `List<${prop.ItemType}>`; + } + return "List"; + } + if (prop.Type === "Map") { + if (prop.PrimitiveItemType) { + return `Map`; + } + if (prop.ItemType) { + return `Map`; + } + return "Map"; + } + if (prop.Type) { + return prop.Type; + } + return "Unknown"; +} + +// Strip HTML tags and decode entities +function stripHtml(html: string): string { + return html + .replace(/<[^>]+>/g, "") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +// Convert HTML to simple markdown +function htmlToMarkdown(html: string): string { + return ( + html + .replace(/([^<]+)<\/code>/g, "`$1`") + .replace(/([^<]+)<\/em>/g, "*$1*") + .replace(/]*>([^<]+)<\/a>/g, "[$2]($1)") + .replace(/

/g, "") + .replace(/<\/p>/g, "\n\n") + .replace(/<[^>]+>/g, "") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, " ") + // Normalize whitespace: collapse multiple spaces/tabs and trim each line + .split("\n") + .map((line) => line.replace(/\s+/g, " ").trim()) + .join("\n") + // Collapse multiple newlines + .replace(/\n{3,}/g, "\n\n") + .trim() + ); +} + +// Concurrency limiter +function createLimiter(concurrency: number) { + let active = 0; + const queue: (() => void)[] = []; + + return async function limit(fn: () => Promise): Promise { + while (active >= concurrency) { + await new Promise((resolve) => queue.push(resolve)); + } + active++; + try { + return await fn(); + } finally { + active--; + const next = queue.shift(); + if (next) next(); + } + }; +} + +// Delay helper +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// Fetch with retry and exponential backoff +async function fetchWithRetry( + url: string, + retries = MAX_RETRIES, +): Promise { + for (let attempt = 0; attempt <= retries; attempt++) { + try { + const res = await fetch(url, { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + }, + }); + + if (res.ok) { + return res; + } + + // Don't retry on 404 - page doesn't exist + if (res.status === 404) { + console.error(` 404 Not Found: ${url}`); + failedRequests++; + return null; + } + + // Retry on rate limiting or server errors + if (res.status === 429 || res.status >= 500) { + const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt); + console.warn( + ` ${res.status} on ${url} - retrying in ${backoffMs}ms (attempt ${attempt + 1}/${retries + 1})`, + ); + await delay(backoffMs); + continue; + } + + // Other errors - log and don't retry + console.error(` HTTP ${res.status} ${res.statusText}: ${url}`); + failedRequests++; + return null; + } catch (error) { + const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt); + console.warn( + ` Network error on ${url} - retrying in ${backoffMs}ms (attempt ${attempt + 1}/${retries + 1}): ${error}`, + ); + await delay(backoffMs); + } + } + + console.error(` Failed after ${retries + 1} attempts: ${url}`); + failedRequests++; + return null; +} + +// Scraped data for a property type (struct) +interface ScrapedPropertyTypeData { + description: string; + propertyDescriptions: Map; +} + +// Scrape property type documentation from AWS HTML +async function scrapePropertyTypeDocs( + docUrl: string, +): Promise { + const res = await fetchWithRetry(docUrl); + if (!res) return null; + + try { + const html = await res.text(); + + const result: ScrapedPropertyTypeData = { + description: "", + propertyDescriptions: new Map(), + }; + + // Extract description - paragraphs after the h1 until the first h2 + const descMatch = html.match( + /<\/awsdocs-filter-selector><\/div>([\s\S]*?)

[\s\S]*?<\/p>/g) || []; + result.description = paragraphs + .map((p) => htmlToMarkdown(p)) + .filter((p) => p.length > 0) + .join("\n\n"); + } + + // Extract property descriptions from the variablelist + const propsMatch = html.match( + /

]*>Properties<\/h2>([\s\S]*?)(?:]*>[\s\S]*?([^<]+)<\/code>[\s\S]*?<\/dt>\s*
([\s\S]*?)<\/dd>/g; + let match; + while ((match = propRegex.exec(propsHtml)) !== null) { + const propName = match[2]; + const propContent = match[3]; + + const descParts = propContent.split(/

Required<\/em>/); + let description = ""; + if (descParts[0]) { + description = htmlToMarkdown(descParts[0]); + } + + let allowedValues: string[] | undefined; + const allowedMatch = propContent.match( + /Allowed values<\/em>:\s*([^<]+)<\/code>/, + ); + if (allowedMatch) { + allowedValues = allowedMatch[1].split(" | ").map((v) => v.trim()); + } + + if (propName) { + result.propertyDescriptions.set(propName, { + description, + allowedValues, + }); + } + } + } + + return result; + } catch (error) { + console.error(` Error parsing ${docUrl}:`, error); + failedRequests++; + return null; + } +} + +// Scrape resource documentation from AWS HTML +async function scrapeResourceDocs( + docUrl: string, +): Promise { + const res = await fetchWithRetry(docUrl); + if (!res) return null; + + try { + const html = await res.text(); + + const result: ScrapedResourceData = { + description: "", + propertyDescriptions: new Map(), + returnValues: [], + examples: [], + }; + + // Extract description - paragraphs after the h1 until the first h2 + const descMatch = html.match( + /<\/awsdocs-filter-selector><\/div>([\s\S]*?)

[\s\S]*?<\/p>/g) || []; + result.description = paragraphs + .map((p) => htmlToMarkdown(p)) + .filter((p) => p.length > 0) + .join("\n\n"); + } + + // Extract property descriptions from the variablelist + const propsMatch = html.match( + /

]*>Properties<\/h2>([\s\S]*?)(?:]*>[\s\S]*?([^<]+)<\/code>[\s\S]*?<\/dt>\s*
([\s\S]*?)<\/dd>/g; + let match; + while ((match = propRegex.exec(propsHtml)) !== null) { + const propName = match[2]; + const propContent = match[3]; + + // Extract the description (first paragraph or text before Required) + const descParts = propContent.split(/

Required<\/em>/); + let description = ""; + if (descParts[0]) { + description = htmlToMarkdown(descParts[0]); + } + + // Extract allowed values if present + let allowedValues: string[] | undefined; + const allowedMatch = propContent.match( + /Allowed values<\/em>:\s*([^<]+)<\/code>/, + ); + if (allowedMatch) { + allowedValues = allowedMatch[1].split(" | ").map((v) => v.trim()); + } + + if (propName && description) { + result.propertyDescriptions.set(propName, { + description, + allowedValues, + }); + } + } + } + + // Extract return values + const returnMatch = html.match( + /

]*>Return values<\/h2>([\s\S]*?)(?:

]*>[\s\S]*?([^<]+)<\/code>[\s\S]*?<\/dt>\s*
([\s\S]*?)<\/dd>/g; + let match; + while ((match = attrRegex.exec(returnHtml)) !== null) { + const attrName = match[2]; + const attrContent = match[3]; + + // Extract description + const descMatch = attrContent.match(/

([\s\S]*?)<\/p>/); + let description = descMatch ? htmlToMarkdown(descMatch[1]) : ""; + + // Extract example if present + let example: string | undefined; + const exampleMatch = attrContent.match( + /Example:\s*([^<]+)<\/code>/, + ); + if (exampleMatch) { + example = exampleMatch[1].trim(); + } + + result.returnValues.push({ name: attrName, description, example }); + } + } + + // Extract examples + const examplesMatch = html.match( + /

]*>Examples<\/h2>([\s\S]*?)(?:

/); + for (let i = 1; i < exampleSections.length && i <= 3; i++) { + // Limit to first 3 examples + const section = exampleSections[i]; + + // Extract title + const titleMatch = section.match(/^([^<]+)[\s\S]*?

([\s\S]*?)<\/p>/); + const description = descMatch ? htmlToMarkdown(descMatch[1]) : ""; + + // Extract JSON code + let json: string | undefined; + const jsonMatch = section.match( + /

([\s\S]*?)<\/code>/, + ); + if (jsonMatch) { + // Preserve newlines in JSON by just stripping tags + json = jsonMatch[1] + .replace(/\{<\/span>/g, "{") + .replace(/<[^>]+>/g, "") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, " ") + .trim(); + } + + // Extract YAML code + let yaml: string | undefined; + const yamlMatch = section.match( + /
([\s\S]*?)<\/code>/, + ); + if (yamlMatch) { + // Preserve newlines in YAML by just stripping tags + yaml = yamlMatch[1] + .replace(/<[^>]+>/g, "") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, " ") + .trim(); + } + + if (title && (json || yaml)) { + result.examples.push({ title, description, json, yaml }); + } + } + } + + return result; + } catch (error) { + console.error(` Error parsing ${docUrl}:`, error); + failedRequests++; + return null; + } +} + +// Generate markdown for a resource +function generateResourceMarkdown( + resourceName: string, + fullType: string, + resource: ResourceType, + relatedStructs: Map, + scraped: ScrapedResourceData | null, + structScraped: Map, +): string { + const lines: string[] = []; + + lines.push(`# ${resourceName}`); + lines.push(""); + lines.push(`CloudFormation Type: \`${fullType}\``); + lines.push(""); + + if (resource.Documentation) { + lines.push(`[AWS Documentation](${resource.Documentation})`); + lines.push(""); + } + + // Add scraped description + if (scraped?.description) { + lines.push(scraped.description); + lines.push(""); + } + + // Properties section + if (resource.Properties && Object.keys(resource.Properties).length > 0) { + lines.push("# Properties"); + lines.push(""); + + const sortedProps = Object.entries(resource.Properties).sort(([a], [b]) => + a.localeCompare(b), + ); + + for (const [name, prop] of sortedProps) { + const type = formatType(prop); + const required = prop.Required ? "Yes" : "No"; + const updateType = prop.UpdateType ?? "N/A"; + + lines.push(`## ${name}`); + lines.push(""); + + // Get scraped description + const scrapedProp = scraped?.propertyDescriptions.get(name); + if (scrapedProp?.description) { + lines.push(scrapedProp.description); + lines.push(""); + } + + lines.push(`- **Required**: ${required}`); + lines.push(`- **Type**: ${type}`); + lines.push(`- **Update**: ${updateType}`); + + // Add allowed values if present + if (scrapedProp?.allowedValues && scrapedProp.allowedValues.length > 0) { + lines.push( + `- **Allowed Values**: \`${scrapedProp.allowedValues.join("` | `")}\``, + ); + } + + lines.push(""); + } + } + + // Attributes section + if (resource.Attributes && Object.keys(resource.Attributes).length > 0) { + lines.push("# Attributes"); + lines.push(""); + + const sortedAttrs = Object.entries(resource.Attributes).sort(([a], [b]) => + a.localeCompare(b), + ); + + for (const [name, attr] of sortedAttrs) { + const type = formatType(attr); + + lines.push(`## ${name}`); + lines.push(""); + + // Find scraped return value description + const returnVal = scraped?.returnValues.find((rv) => rv.name === name); + if (returnVal?.description) { + lines.push(returnVal.description); + lines.push(""); + } + + lines.push(`- **Type**: ${type}`); + if (returnVal?.example) { + lines.push(`- **Example**: \`${returnVal.example}\``); + } + lines.push(""); + } + } + + // Examples section + if (scraped?.examples && scraped.examples.length > 0) { + lines.push("# Examples"); + lines.push(""); + + for (const example of scraped.examples) { + lines.push(`## ${example.title}`); + lines.push(""); + if (example.description) { + lines.push(example.description); + lines.push(""); + } + if (example.yaml) { + lines.push("```yaml"); + lines.push(example.yaml); + lines.push("```"); + lines.push(""); + } else if (example.json) { + lines.push("```json"); + lines.push(example.json); + lines.push("```"); + lines.push(""); + } + } + } + + // Related structs section + if (relatedStructs.size > 0) { + lines.push("# Property Types"); + lines.push(""); + + const sortedStructs = Array.from(relatedStructs.entries()).sort( + ([a], [b]) => a.localeCompare(b), + ); + + for (const [structName, struct] of sortedStructs) { + lines.push(`## ${structName}`); + lines.push(""); + + // Get scraped description for this struct + const scrapedStruct = structScraped.get(structName); + if (scrapedStruct?.description) { + lines.push(scrapedStruct.description); + lines.push(""); + } + + if (struct.Properties && Object.keys(struct.Properties).length > 0) { + const sortedProps = Object.entries(struct.Properties).sort(([a], [b]) => + a.localeCompare(b), + ); + + for (const [name, prop] of sortedProps) { + const type = formatType(prop); + const required = prop.Required ? "Yes" : "No"; + const updateType = prop.UpdateType ?? "N/A"; + + lines.push(`### ${name}`); + lines.push(""); + + // Get scraped property description + const scrapedProp = scrapedStruct?.propertyDescriptions.get(name); + if (scrapedProp?.description) { + lines.push(scrapedProp.description); + lines.push(""); + } + + lines.push(`- **Required**: ${required}`); + lines.push(`- **Type**: ${type}`); + lines.push(`- **Update**: ${updateType}`); + + // Add allowed values if present + if ( + scrapedProp?.allowedValues && + scrapedProp.allowedValues.length > 0 + ) { + lines.push( + `- **Allowed Values**: \`${scrapedProp.allowedValues.join("` | `")}\``, + ); + } + + lines.push(""); + } + } + } + } + + return lines.join("\n"); +} + +async function main() { + console.log("Fetching CloudFormation spec..."); + const res = await fetch(CFN_SPEC_URL); + const json = (await res.json()) as CFNSpec; + + console.log(`Version: ${json.ResourceSpecificationVersion}`); + console.log(`Resources: ${Object.keys(json.ResourceTypes).length}`); + console.log(`PropertyTypes: ${Object.keys(json.PropertyTypes).length}`); + + const cfnDir = ".external/cfn"; + + // Ensure directory exists (don't delete existing files) + await fs.mkdir(cfnDir, { recursive: true }); + + // Group resources by service + const serviceResources = new Map< + string, + Map + >(); + const servicePropertyTypes = new Map>(); + + for (const [fullType, resource] of Object.entries(json.ResourceTypes)) { + const parsed = parseResourceType(fullType); + if (!parsed) continue; + + const { service, resource: resourceName } = parsed; + if (!serviceResources.has(service)) { + serviceResources.set(service, new Map()); + } + serviceResources.get(service)!.set(resourceName, { + ...resource, + _fullType: fullType, + }); + } + + // Group property types by service and resource + for (const [fullType, propType] of Object.entries(json.PropertyTypes)) { + const parsed = parsePropertyType(fullType); + if (!parsed) continue; + + const { service, resource } = parsed; + const key = `${service}::${resource}`; + if (!servicePropertyTypes.has(key)) { + servicePropertyTypes.set(key, new Map()); + } + servicePropertyTypes.get(key)!.set(parsed.propertyType, propType); + } + + console.log(`\nGenerating markdown for ${serviceResources.size} services...`); + console.log( + `(Scraping AWS documentation with ${CONCURRENCY} concurrent requests...)\n`, + ); + + // Create service directories first + for (const service of serviceResources.keys()) { + const serviceDir = path.join(cfnDir, toKebabCase(service)); + await fs.mkdir(serviceDir, { recursive: true }); + } + + // Collect all resources to process + const allResources: Array<{ + service: string; + resourceName: string; + resource: ResourceType & { _fullType: string }; + relatedStructs: Map; + }> = []; + + for (const [service, resources] of serviceResources) { + for (const [resourceName, resource] of resources) { + const key = `${service}::${resourceName}`; + const relatedStructs = servicePropertyTypes.get(key) ?? new Map(); + allResources.push({ service, resourceName, resource, relatedStructs }); + } + } + + // Helper to check if file exists + async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + } + + // Filter to only resources that need processing + const resourcesToProcess: typeof allResources = []; + let skippedCount = 0; + + for (const item of allResources) { + const serviceDir = path.join(cfnDir, toKebabCase(item.service)); + const filePath = path.join(serviceDir, `${item.resourceName}.md`); + if (await fileExists(filePath)) { + console.log(`Skipping ${filePath} (already exists)`); + skippedCount++; + } else { + resourcesToProcess.push(item); + } + } + + if (skippedCount > 0) { + console.log(`Skipping ${skippedCount} resources (already exist)`); + } + + const totalToProcess = resourcesToProcess.length; + if (totalToProcess === 0) { + console.log("All resources already processed. Nothing to do."); + return; + } + + console.log(`Processing ${totalToProcess} resources...\n`); + + let processedResources = 0; + let scrapedCount = 0; + + // Concurrency limiter for HTTP requests + const limit = createLimiter(CONCURRENCY); + + // Process all resources in parallel with limited concurrency + await Promise.all( + resourcesToProcess.map( + async ({ service, resourceName, resource, relatedStructs }) => { + const fullType = resource._fullType; + + // Scrape the AWS documentation (with concurrency limit) + let scraped: ScrapedResourceData | null = null; + if (resource.Documentation) { + scraped = await limit(() => + scrapeResourceDocs(resource.Documentation!), + ); + if (scraped) { + scrapedCount++; + } + } + + // Scrape property type documentation in parallel + const structEntries = Array.from(relatedStructs.entries()).filter( + ([, struct]) => struct.Documentation, + ); + const scrapedStructResults = await Promise.all( + structEntries.map(async ([structName, struct]) => { + const scrapedStruct = await limit(() => + scrapePropertyTypeDocs(struct.Documentation!), + ); + return [structName, scrapedStruct] as const; + }), + ); + + const structScraped = new Map(); + for (const [structName, scrapedStruct] of scrapedStructResults) { + if (scrapedStruct) { + structScraped.set(structName, scrapedStruct); + } + } + + const markdown = generateResourceMarkdown( + resourceName, + fullType, + resource, + relatedStructs, + scraped, + structScraped, + ); + + const serviceDir = path.join(cfnDir, toKebabCase(service)); + const filePath = path.join(serviceDir, `${resourceName}.md`); + await fs.writeFile(filePath, markdown); + + processedResources++; + if (processedResources % 100 === 0) { + console.log( + ` Progress: ${processedResources}/${totalToProcess} (${Math.round((processedResources / totalToProcess) * 100)}%)`, + ); + } + }, + ), + ); + + console.log(`\nDone! Generated ${totalToProcess} resource docs.`); + console.log(`Successfully scraped ${scrapedCount} AWS documentation pages.`); + + if (failedRequests > 0) { + console.warn( + `\n⚠️ Warning: ${failedRequests} requests failed - some docs may be incomplete`, + ); + process.exit(1); + } +} + +main().catch(console.error); diff --git a/.repos/alchemy-effect/scripts/generate-docs.ts b/.repos/alchemy-effect/scripts/generate-docs.ts new file mode 100644 index 00000000000..6e45dab3ac1 --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-docs.ts @@ -0,0 +1,47 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; + +import { + chooseCanonicalEntries, + createProject, + discoverSourceFiles, +} from "./generate-docs/discovery.ts"; +import { buildFileDoc } from "./generate-docs/model.ts"; +import { renderFileDoc, syntheticIndexes } from "./generate-docs/render.ts"; +import { docsRoot } from "./generate-docs/utils.ts"; + +async function writeFile(targetPath: string, content: string) { + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await fs.writeFile(targetPath, `${content.trimEnd()}\n`, "utf8"); +} + +async function main() { + const project = createProject(); + const sourceFiles = discoverSourceFiles(project); + const { entries, duplicates } = chooseCanonicalEntries(sourceFiles); + + await fs.rm(docsRoot, { recursive: true, force: true }); + await fs.mkdir(docsRoot, { recursive: true }); + + for (const entry of entries) { + const fileDoc = buildFileDoc(project, entry, entries); + await writeFile(entry.outputPath, renderFileDoc(fileDoc)); + } + + for (const synthetic of syntheticIndexes(entries)) { + await writeFile(synthetic.outputPath, synthetic.content); + } + + console.log(`Generated ${entries.length} file docs.`); + if (duplicates.length > 0) { + console.log("Ignored case-colliding duplicates:"); + for (const duplicate of duplicates) { + console.log(`- kept ${duplicate.canonical}`); + for (const ignored of duplicate.ignored) { + console.log(` - ignored ${ignored}`); + } + } + } +} + +await main(); diff --git a/.repos/alchemy-effect/scripts/generate-docs/discovery.ts b/.repos/alchemy-effect/scripts/generate-docs/discovery.ts new file mode 100644 index 00000000000..352a9c9ac3a --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-docs/discovery.ts @@ -0,0 +1,279 @@ +import * as path from "node:path"; + +import { + Node, + Project, + QuoteKind, + SyntaxKind, + type ClassDeclaration, + type SourceFile, + type VariableDeclaration, +} from "ts-morph"; + +import type { DuplicateGroup, FileKind, SourceEntry } from "./types.ts"; +import { + canonicalScore, + docOutputPath, + lowerPathKey, + srcRoot, + titleFromRelativePath, + tsConfigPath, +} from "./utils.ts"; + +export function createProject() { + return new Project({ + tsConfigFilePath: tsConfigPath, + skipFileDependencyResolution: true, + manipulationSettings: { + quoteKind: QuoteKind.Double, + }, + }); +} + +export function discoverSourceFiles(project: Project) { + return project + .getSourceFiles() + .filter( + (sourceFile) => + sourceFile.getFilePath().startsWith(srcRoot) && + !sourceFile.isDeclarationFile(), + ); +} + +export function chooseCanonicalEntries(sourceFiles: SourceFile[]) { + const groups = new Map(); + for (const sourceFile of sourceFiles) { + const relativePath = path.relative(srcRoot, sourceFile.getFilePath()); + const key = lowerPathKey(relativePath); + const group = groups.get(key); + if (group) { + group.push(sourceFile); + } else { + groups.set(key, [sourceFile]); + } + } + + const entries: SourceEntry[] = []; + const duplicates: DuplicateGroup[] = []; + + for (const group of groups.values()) { + const sorted = [...group].sort((left, right) => { + const leftRelative = path.relative(srcRoot, left.getFilePath()); + const rightRelative = path.relative(srcRoot, right.getFilePath()); + return ( + canonicalScore(rightRelative) - canonicalScore(leftRelative) || + leftRelative.localeCompare(rightRelative) + ); + }); + const canonical = sorted[0]!; + const relativePath = path.relative(srcRoot, canonical.getFilePath()); + entries.push({ + relativePath, + outputPath: docOutputPath(relativePath), + sourcePath: canonical.getFilePath(), + }); + + if (sorted.length > 1) { + duplicates.push({ + canonical: relativePath, + ignored: sorted + .slice(1) + .map((file) => path.relative(srcRoot, file.getFilePath())), + }); + } + } + + entries.sort((left, right) => + left.relativePath.localeCompare(right.relativePath), + ); + duplicates.sort((left, right) => + left.canonical.localeCompare(right.canonical), + ); + + return { entries, duplicates }; +} + +export function sourceFileForEntry(project: Project, entry: SourceEntry) { + return project.getSourceFileOrThrow(entry.sourcePath); +} + +export function getResourceFactory( + sourceFile: SourceFile, + factoryName: "Resource" | "Host", +) { + return sourceFile.getVariableDeclarations().find((declaration) => { + if (!declaration.isExported()) { + return false; + } + const initializer = declaration.getInitializerIfKind( + SyntaxKind.CallExpression, + ); + return initializer?.getExpression().getText() === factoryName; + }); +} + +export function getResourceTypeDetails( + sourceFile: SourceFile, + factoryName: "Resource" | "Host", +) { + const declaration = getResourceFactory(sourceFile, factoryName); + if (!declaration) { + return undefined; + } + const call = declaration.getInitializerIfKindOrThrow( + SyntaxKind.CallExpression, + ); + const firstArg = call.getArguments()[0]; + return { + declaration, + name: declaration.getName(), + resourceType: + firstArg && Node.isStringLiteral(firstArg) + ? firstArg.getLiteralValue() + : declaration.getName(), + }; +} + +function extendsCall(declaration: ClassDeclaration, expressionText: string) { + return declaration.getText().includes(`extends ${expressionText}<`); +} + +export function getBindingServiceClasses(sourceFile: SourceFile) { + return sourceFile + .getClasses() + .filter( + (declaration) => + declaration.isExported() && extendsCall(declaration, "Binding.Service"), + ); +} + +export function getBindingPolicyClasses(sourceFile: SourceFile) { + return sourceFile + .getClasses() + .filter( + (declaration) => + declaration.isExported() && extendsCall(declaration, "Binding.Policy"), + ); +} + +export function getExportedNames(sourceFile: SourceFile) { + return [ + ...sourceFile + .getClasses() + .filter((item) => item.isExported()) + .map((item) => item.getName() ?? ""), + ...sourceFile + .getFunctions() + .filter((item) => item.isExported()) + .map((item) => item.getName() ?? ""), + ...sourceFile + .getInterfaces() + .filter((item) => item.isExported()) + .map((item) => item.getName()), + ...sourceFile + .getTypeAliases() + .filter((item) => item.isExported()) + .map((item) => item.getName()), + ...sourceFile + .getVariableDeclarations() + .filter((item) => item.isExported()) + .map((item) => item.getName()), + ].filter(Boolean); +} + +export function getFileKind(sourceFile: SourceFile): FileKind { + const baseName = sourceFile.getBaseNameWithoutExtension(); + const exportedNames = getExportedNames(sourceFile).join(" "); + const eventish = + /EventSource|Sink/.test(baseName) || /EventSource|Sink/.test(exportedNames); + + if (baseName === "index") { + return "index"; + } + if (getResourceFactory(sourceFile, "Host")) { + return "host"; + } + if (getResourceFactory(sourceFile, "Resource")) { + return "resource"; + } + if ( + eventish && + (getBindingPolicyClasses(sourceFile).length > 0 || + getBindingServiceClasses(sourceFile).length > 0 || + sourceFile.getText().includes("Layer.effect(")) + ) { + return "event-source"; + } + if ( + getBindingServiceClasses(sourceFile).length > 0 || + getBindingPolicyClasses(sourceFile).length > 0 + ) { + return "operation"; + } + if ( + baseName === "Providers" || + sourceFile + .getVariableDeclarations() + .some( + (declaration) => + declaration.isExported() && + ["providers", "resources", "bindings"].includes( + declaration.getName(), + ), + ) + ) { + return "provider"; + } + return "helper"; +} + +export function getProviderDeclaration( + sourceFile: SourceFile, + name: string, +): VariableDeclaration | undefined { + return sourceFile.getVariableDeclaration(`${name}Provider`); +} + +export function getPrimaryNode(sourceFile: SourceFile, fileKind: FileKind) { + switch (fileKind) { + case "resource": + return getResourceFactory(sourceFile, "Resource")?.getVariableStatement(); + case "host": + return getResourceFactory(sourceFile, "Host")?.getVariableStatement(); + case "operation": + return ( + getBindingServiceClasses(sourceFile)[0] ?? + getBindingPolicyClasses(sourceFile)[0] + ); + case "event-source": + return ( + sourceFile + .getVariableDeclarations() + .find( + (declaration) => + declaration.isExported() && + /EventSource|Sink/.test(declaration.getName()), + ) + ?.getVariableStatement() ?? getBindingPolicyClasses(sourceFile)[0] + ); + case "provider": + return sourceFile + .getVariableDeclarations() + .find((declaration) => declaration.isExported()) + ?.getVariableStatement(); + case "index": + return sourceFile.getExportDeclarations()[0]; + default: + return sourceFile + .getStatements() + .find( + (statement) => Node.isExportable(statement) && statement.isExported(), + ); + } +} + +export function titleForSourceFile(sourceFile: SourceFile) { + return titleFromRelativePath( + path.relative(srcRoot, sourceFile.getFilePath()), + ); +} diff --git a/.repos/alchemy-effect/scripts/generate-docs/model.ts b/.repos/alchemy-effect/scripts/generate-docs/model.ts new file mode 100644 index 00000000000..a8c76e37014 --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-docs/model.ts @@ -0,0 +1,814 @@ +import * as path from "node:path"; + +import { + Node, + SyntaxKind, + type ClassDeclaration, + type InterfaceDeclaration, + type ParameterDeclaration, + type SourceFile, + type Symbol, + type TypeAliasDeclaration, + type TypeNode, +} from "ts-morph"; + +import { + getBindingPolicyClasses, + getBindingServiceClasses, + getFileKind, + getPrimaryNode, + getProviderDeclaration, + getResourceTypeDetails, +} from "./discovery.ts"; +import { + docsRoot, + formatTypeText, + getDefaultValue, + getJSDocInfo, + getSummary, + guessExampleValue, + labelFromDocRelativePath, + lowerCamel, + relativeDocLink, + relativeSourcePath, + srcRoot, + titleFromRelativePath, + truncateInline, +} from "./utils.ts"; +import type { + BindingClassDoc, + DirectoryCatalog, + ExportDoc, + FileDoc, + FileKind, + LinkDoc, + OperationDoc, + PropertyDoc, + ResourceDoc, + ShapeDoc, + SourceEntry, +} from "./types.ts"; +import { lifecycleOperationOrder } from "./types.ts"; + +function propertySignatureToDoc( + property: import("ts-morph").PropertySignature, +): PropertyDoc { + return { + name: property.getName(), + type: formatTypeText( + property.getTypeNode()?.getText() ?? property.getType().getText(property), + ), + optional: property.hasQuestionToken(), + readonly: property.isReadonly(), + description: getSummary(property), + defaultValue: getDefaultValue(property), + }; +} + +function getShapeFromMembers( + title: string, + members: Node[], + description?: string, +): ShapeDoc | undefined { + const properties = members + .flatMap((member) => + Node.isPropertySignature(member) ? [propertySignatureToDoc(member)] : [], + ) + .sort((left, right) => left.name.localeCompare(right.name)); + + if (properties.length === 0) { + return undefined; + } + + return { + title, + description, + properties, + }; +} + +function resolveShapeDeclaration( + symbol: Symbol | undefined, +): InterfaceDeclaration | TypeAliasDeclaration | undefined { + return symbol + ?.getDeclarations() + .find( + (declaration) => + Node.isInterfaceDeclaration(declaration) || + Node.isTypeAliasDeclaration(declaration), + ) as InterfaceDeclaration | TypeAliasDeclaration | undefined; +} + +function getShapeFromTypeNode( + title: string, + typeNode: TypeNode, + description?: string, +): ShapeDoc | undefined { + if (Node.isTypeLiteral(typeNode)) { + return getShapeFromMembers(title, typeNode.getMembers(), description); + } + + if (Node.isTypeReference(typeNode)) { + const declaration = resolveShapeDeclaration( + typeNode.getTypeName().getSymbol(), + ); + if (declaration) { + return getShapeFromDeclaration(title, declaration); + } + } + + return { + title, + description, + properties: [], + signature: formatTypeText(typeNode.getText()), + }; +} + +function getShapeFromDeclaration( + title: string, + declaration: InterfaceDeclaration | TypeAliasDeclaration, +): ShapeDoc | undefined { + if (Node.isInterfaceDeclaration(declaration)) { + return getShapeFromMembers( + title, + declaration.getMembers(), + getSummary(declaration), + ); + } + + const typeNode = declaration.getTypeNode(); + if (!typeNode) { + return undefined; + } + return getShapeFromTypeNode(title, typeNode, getSummary(declaration)); +} + +function signatureForDeclaration(declaration: Node) { + if (Node.isClassDeclaration(declaration)) { + const heritage = declaration + .getHeritageClauses() + .flatMap((clause) => clause.getTypeNodes()) + .map((typeNode) => typeNode.getText()) + .join(", "); + return `class ${declaration.getName() ?? "Anonymous"}${heritage ? ` extends ${heritage}` : ""}`; + } + + if (Node.isFunctionDeclaration(declaration)) { + const params = declaration + .getParameters() + .map((param) => param.getText()) + .join(", "); + const returnType = declaration.getReturnTypeNode()?.getText(); + return `function ${declaration.getName() ?? "anonymous"}(${params})${returnType ? `: ${returnType}` : ""}`; + } + + if (Node.isInterfaceDeclaration(declaration)) { + return `interface ${declaration.getName()}`; + } + + if (Node.isTypeAliasDeclaration(declaration)) { + return `type ${declaration.getName()} = ${formatTypeText( + declaration.getTypeNode()?.getText(), + )}`; + } + + if (Node.isVariableDeclaration(declaration)) { + const kind = + declaration.getVariableStatement()?.getDeclarationKind() ?? "const"; + const initializer = declaration.getInitializer()?.getText(); + return `${kind} ${declaration.getName()}${initializer ? ` = ${truncateInline(initializer)}` : ""}`; + } + + if (Node.isModuleDeclaration(declaration)) { + return `namespace ${declaration.getName()}`; + } + + if (Node.isEnumDeclaration(declaration)) { + return `enum ${declaration.getName()}`; + } + + return truncateInline(declaration.getText()); +} + +function getLocalExports(sourceFile: SourceFile): ExportDoc[] { + const exports: ExportDoc[] = []; + + for (const declaration of sourceFile + .getClasses() + .filter((item) => item.isExported())) { + exports.push({ + name: declaration.getName() ?? "Anonymous", + kind: "class", + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration), + }); + } + + for (const declaration of sourceFile + .getFunctions() + .filter((item) => item.isExported())) { + exports.push({ + name: declaration.getName() ?? "anonymous", + kind: "function", + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration), + }); + } + + for (const declaration of sourceFile + .getInterfaces() + .filter((item) => item.isExported())) { + exports.push({ + name: declaration.getName(), + kind: "interface", + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration), + shape: getShapeFromDeclaration(declaration.getName(), declaration), + }); + } + + for (const declaration of sourceFile + .getTypeAliases() + .filter((item) => item.isExported())) { + exports.push({ + name: declaration.getName(), + kind: "type", + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration), + shape: getShapeFromDeclaration(declaration.getName(), declaration), + }); + } + + for (const declaration of sourceFile + .getVariableDeclarations() + .filter((item) => item.isExported())) { + exports.push({ + name: declaration.getName(), + kind: "const", + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration.getVariableStatement() ?? declaration), + }); + } + + for (const declaration of sourceFile + .getEnums() + .filter((item) => item.isExported())) { + exports.push({ + name: declaration.getName(), + kind: "enum", + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration), + }); + } + + for (const declaration of sourceFile + .getModules() + .filter((item) => item.isExported())) { + exports.push({ + name: declaration.getName(), + kind: "namespace", + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration), + }); + } + + const seen = new Set(); + return exports + .filter((entry) => { + const key = `${entry.kind}:${entry.name}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }) + .sort((left, right) => left.name.localeCompare(right.name)); +} + +function getLifecycleOperations(sourceFile: SourceFile, resourceName: string) { + const provider = getProviderDeclaration(sourceFile, resourceName); + if (!provider) { + return []; + } + const text = provider.getText(); + return lifecycleOperationOrder.filter((name) => + new RegExp(`\\b${name}\\s*:`).test(text), + ); +} + +function getResourceDoc(sourceFile: SourceFile, fileKind: FileKind) { + const details = getResourceTypeDetails( + sourceFile, + fileKind === "host" ? "Host" : "Resource", + ); + if (!details) { + return undefined; + } + + const resourceInterface = sourceFile.getInterface(details.name); + const props = + sourceFile.getInterface(`${details.name}Props`) ?? + sourceFile.getTypeAlias(`${details.name}Props`); + + let attributes: ShapeDoc | undefined; + let binding: ShapeDoc | undefined; + + if (resourceInterface) { + const resourceHeritage = resourceInterface + .getHeritageClauses() + .flatMap((clause) => clause.getTypeNodes()) + .find((typeNode) => typeNode.getExpression().getText() === "Resource"); + + if (resourceHeritage) { + const typeArguments = resourceHeritage.getTypeArguments(); + const attrsNode = typeArguments[2]; + const bindingNode = typeArguments[3]; + if (attrsNode) { + attributes = getShapeFromTypeNode( + `${details.name} Attributes`, + attrsNode, + ); + } + if (bindingNode) { + binding = getShapeFromTypeNode( + `${details.name} Binding Contract`, + bindingNode, + ); + } + } + } + + const resourceDoc: ResourceDoc = { + name: details.name, + resourceType: details.resourceType, + props: props + ? getShapeFromDeclaration(`${details.name} Props`, props) + : undefined, + attributes, + binding, + lifecycleOperations: getLifecycleOperations(sourceFile, details.name), + providerName: getProviderDeclaration(sourceFile, details.name)?.getName(), + }; + + return resourceDoc; +} + +function getBindingIdentifier(declaration: ClassDeclaration) { + const text = declaration.getText(); + return text.match(/\)\("([^"]+)"\)/)?.[1]; +} + +function bindingDocForClass(declaration: ClassDeclaration): BindingClassDoc { + return { + name: declaration.getName() ?? "Anonymous", + identifier: getBindingIdentifier(declaration), + signature: signatureForDeclaration(declaration), + summary: getSummary(declaration), + }; +} + +function getRequestShapes(sourceFile: SourceFile) { + return [ + ...sourceFile + .getInterfaces() + .filter( + (item) => + item.isExported() && + /Request|Props|Options|Input$/.test(item.getName()), + ), + ...sourceFile + .getTypeAliases() + .filter( + (item) => + item.isExported() && + /Request|Props|Options|Input$/.test(item.getName()), + ), + ] + .map((declaration) => + getShapeFromDeclaration(declaration.getName(), declaration), + ) + .filter((shape): shape is ShapeDoc => shape !== undefined) + .sort((left, right) => left.title.localeCompare(right.title)); +} + +function getSupportedHosts(sourceFile: SourceFile) { + const hosts = new Set(); + const text = sourceFile.getText(); + if (text.includes("isFunction(host)")) { + hosts.add("AWS.Lambda.Function"); + } + if (text.includes("isWorker(host)")) { + hosts.add("Cloudflare.Workers.Worker"); + } + return [...hosts].sort(); +} + +function parameterToUsage(parameter: ParameterDeclaration) { + return { + name: parameter.getName(), + type: formatTypeText( + parameter.getTypeNode()?.getText() ?? + parameter.getType().getText(parameter), + ), + optional: parameter.isOptional(), + rest: parameter.isRestParameter(), + }; +} + +function getOperationUsage( + sourceFile: SourceFile, + serviceName: string | undefined, +) { + if (!serviceName) { + return undefined; + } + + const live = sourceFile.getVariableDeclaration(`${serviceName}Live`); + const initializer = live?.getInitializer(); + if (!initializer) { + return undefined; + } + + const effectFns = initializer + .getDescendantsOfKind(SyntaxKind.CallExpression) + .filter((call) => call.getExpression().getText() === "Effect.fn"); + + const [bindFn, invokeFn] = effectFns.map((call) => { + const firstArg = call.getArguments()[0]; + if ( + firstArg && + (Node.isFunctionExpression(firstArg) || Node.isArrowFunction(firstArg)) + ) { + return firstArg; + } + return undefined; + }); + + if (!bindFn && !invokeFn) { + return undefined; + } + + return { + bindParameters: bindFn?.getParameters().map(parameterToUsage) ?? [], + invokeParameters: invokeFn?.getParameters().map(parameterToUsage) ?? [], + }; +} + +function getOperationDoc(sourceFile: SourceFile): OperationDoc | undefined { + const services = getBindingServiceClasses(sourceFile).map(bindingDocForClass); + const policies = getBindingPolicyClasses(sourceFile).map(bindingDocForClass); + const runtimeLayers = sourceFile + .getVariableDeclarations() + .filter( + (declaration) => + declaration.isExported() && + /Live$/.test(declaration.getName()) && + declaration.getInitializer()?.getText().includes("Layer.") === true, + ) + .map((declaration) => declaration.getName()) + .sort(); + const requestShapes = getRequestShapes(sourceFile); + + if ( + services.length === 0 && + policies.length === 0 && + runtimeLayers.length === 0 && + requestShapes.length === 0 + ) { + return undefined; + } + + return { + services, + policies, + runtimeLayers, + supportedHosts: getSupportedHosts(sourceFile), + requestShapes, + usage: getOperationUsage(sourceFile, services[0]?.name), + }; +} + +function getIndexDoc(sourceFile: SourceFile, outputPath: string) { + return { + reExports: sourceFile + .getExportDeclarations() + .flatMap((declaration) => { + const moduleSpecifier = declaration.getModuleSpecifierValue(); + if (!moduleSpecifier) { + return []; + } + const target = declaration.getModuleSpecifierSourceFile(); + const href = target + ? relativeDocLink( + outputPath, + path.join( + docsRoot, + path + .relative(srcRoot, target.getFilePath()) + .replace(/\.ts$/, ".md"), + ), + ) + : undefined; + const namespaceExport = declaration.getNamespaceExport(); + const exportNames = + declaration.getNamedExports().length > 0 + ? declaration.getNamedExports().map((item) => item.getName()) + : namespaceExport + ? [namespaceExport.getName()] + : ["*"]; + + return exportNames.map((exportName) => ({ + exportName, + sourcePath: moduleSpecifier, + href, + })); + }) + .sort((left, right) => + `${left.exportName}:${left.sourcePath}`.localeCompare( + `${right.exportName}:${right.sourcePath}`, + ), + ), + }; +} + +function getProviderDoc(sourceFile: SourceFile) { + const exportedFactories = sourceFile + .getVariableDeclarations() + .filter((declaration) => declaration.isExported()) + .map((declaration) => declaration.getName()) + .filter( + (name) => + [ + "providers", + "resources", + "bindings", + "credentials", + "stageConfig", + ].includes(name) || /Provider/.test(name), + ) + .sort(); + + return exportedFactories.length > 0 ? { exportedFactories } : undefined; +} + +function buildRelatedLinks( + sourceFile: SourceFile, + outputPath: string, +): LinkDoc[] { + const links = new Map(); + + const maybeAdd = (target: SourceFile | undefined, label: string) => { + if (!target || !target.getFilePath().startsWith(srcRoot)) { + return; + } + const relativeTargetPath = path.relative(srcRoot, target.getFilePath()); + const href = relativeDocLink( + outputPath, + path.join(docsRoot, relativeTargetPath.replace(/\.ts$/, ".md")), + ); + links.set(`${label}:${href}`, { + label: + path.basename(relativeTargetPath, ".ts") === "index" + ? path.basename(path.dirname(relativeTargetPath)) + : label, + href, + }); + }; + + for (const declaration of sourceFile.getImportDeclarations()) { + maybeAdd( + declaration.getModuleSpecifierSourceFile(), + path.basename(declaration.getModuleSpecifierValue(), ".ts"), + ); + } + + for (const declaration of sourceFile.getExportDeclarations()) { + const moduleSpecifier = declaration.getModuleSpecifierValue() ?? "index.ts"; + maybeAdd( + declaration.getModuleSpecifierSourceFile(), + path.basename(moduleSpecifier, ".ts"), + ); + } + + return [...links.values()].sort((left, right) => + left.label.localeCompare(right.label), + ); +} + +function buildDirectoryCatalog( + entry: SourceEntry, + entries: SourceEntry[], +): DirectoryCatalog { + const directory = path.dirname(entry.relativePath); + const isIndexPage = path.basename(entry.relativePath) === "index.ts"; + const parent = !isIndexPage + ? { + label: directory === "." ? "API Reference" : path.basename(directory), + href: relativeDocLink( + entry.outputPath, + path.join( + docsRoot, + directory === "." ? "index.md" : directory, + "index.md", + ), + ), + } + : directory === "." + ? undefined + : { + label: + path.dirname(directory) === "." + ? "API Reference" + : path.basename(path.dirname(directory)), + href: relativeDocLink( + entry.outputPath, + path.join( + docsRoot, + path.dirname(directory) === "." + ? "index.md" + : path.dirname(directory), + "index.md", + ), + ), + }; + + const siblings = entries + .filter( + (candidate) => + path.dirname(candidate.relativePath) === directory && + candidate.relativePath !== entry.relativePath, + ) + .map((candidate) => ({ + label: labelFromDocRelativePath(candidate.relativePath), + href: relativeDocLink(entry.outputPath, candidate.outputPath), + })) + .sort((left, right) => left.label.localeCompare(right.label)); + + return { parent, siblings }; +} + +function buildSummary(sourceFile: SourceFile, fileKind: FileKind) { + const primaryNode = getPrimaryNode(sourceFile, fileKind); + const summary = primaryNode ? getSummary(primaryNode) : undefined; + if (summary) { + return summary; + } + return ""; +} + +function singularize(name: string) { + return name.endsWith("ies") + ? `${name.slice(0, -3)}y` + : name.endsWith("ses") + ? name.slice(0, -2) + : name.endsWith("s") && name.length > 1 + ? name.slice(0, -1) + : name; +} + +function guessBindingArgument(name: string, rest: boolean) { + const base = rest ? singularize(name) : name; + return lowerCamel(base || "resource"); +} + +function buildRequestBlock(shape: ShapeDoc | undefined) { + if (!shape) { + return "{\n // request fields\n}"; + } + + const required = shape.properties.filter((property) => !property.optional); + const properties = (required.length > 0 ? required : shape.properties).slice( + 0, + 4, + ); + + if (properties.length === 0) { + return "{}"; + } + + return `{\n${properties + .map((property) => ` ${property.name}: ${guessExampleValue(property)},`) + .join("\n")}\n}`; +} + +function buildAutoExample( + fileKind: FileKind, + title: string, + resource?: ResourceDoc, + operation?: OperationDoc, +) { + if (fileKind === "resource" || fileKind === "host") { + const required = (resource?.props?.properties ?? []).filter( + (property) => !property.optional, + ); + const propsBlock = + required.length === 0 + ? "{}" + : `{\n${required + .map( + (property) => + ` ${property.name}: ${guessExampleValue(property)},`, + ) + .join("\n")}\n}`; + return { + title: `Create ${title}`, + body: [ + "```typescript", + `const ${lowerCamel(title)} = yield* ${title}("${title}", ${propsBlock});`, + "```", + ].join("\n"), + }; + } + + if (fileKind === "operation") { + const service = operation?.services[0]?.name ?? title; + const bindArguments = + operation?.usage?.bindParameters.map((parameter) => + guessBindingArgument(parameter.name, parameter.rest), + ) ?? []; + const bindCall = + bindArguments.length > 0 + ? `.bind(${bindArguments.join(", ")})` + : ".bind()"; + const invokeParameter = operation?.usage?.invokeParameters[0]; + const requestShape = operation?.requestShapes[0]; + const invocation = invokeParameter + ? invokeParameter.optional + ? `const response = yield* ${lowerCamel(service)}();` + : `const response = yield* ${lowerCamel(service)}(${buildRequestBlock(requestShape)});` + : `const response = yield* ${lowerCamel(service)}();`; + + return { + title: `Use ${service}`, + body: [ + "```typescript", + `const ${lowerCamel(service)} = yield* ${service}${bindCall};`, + "", + invocation, + "```", + ].join("\n"), + }; + } + + if (fileKind === "event-source") { + return { + title: `Attach ${title}`, + body: [ + "```typescript", + `yield* ${title}.bind(resource, {}, (events) =>`, + " Effect.log(events),", + ");", + "```", + ].join("\n"), + }; + } + + return undefined; +} + +export function buildFileDoc( + project: import("ts-morph").Project, + entry: SourceEntry, + entries: SourceEntry[], +): FileDoc { + const sourceFile = project.getSourceFileOrThrow(entry.sourcePath); + const fileKind = getFileKind(sourceFile); + const title = titleFromRelativePath(entry.relativePath); + const examples = (() => { + const primaryNode = getPrimaryNode(sourceFile, fileKind); + return primaryNode ? getJSDocInfo(primaryNode).sections : []; + })(); + const resource = + fileKind === "resource" || fileKind === "host" + ? getResourceDoc(sourceFile, fileKind) + : undefined; + const operation = + fileKind === "operation" || fileKind === "event-source" + ? getOperationDoc(sourceFile) + : undefined; + + return { + title, + fileKind, + summary: buildSummary(sourceFile, fileKind), + sourcePath: relativeSourcePath(sourceFile.getFilePath()), + relativePath: entry.relativePath, + outputPath: entry.outputPath, + exports: getLocalExports(sourceFile), + resource, + operation, + index: + fileKind === "index" + ? getIndexDoc(sourceFile, entry.outputPath) + : undefined, + provider: fileKind === "provider" ? getProviderDoc(sourceFile) : undefined, + examples, + autoExample: + examples.length === 0 + ? buildAutoExample(fileKind, title, resource, operation) + : undefined, + relatedLinks: buildRelatedLinks(sourceFile, entry.outputPath), + directoryCatalog: buildDirectoryCatalog(entry, entries), + }; +} diff --git a/.repos/alchemy-effect/scripts/generate-docs/render.ts b/.repos/alchemy-effect/scripts/generate-docs/render.ts new file mode 100644 index 00000000000..768c9e0b1dc --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-docs/render.ts @@ -0,0 +1,330 @@ +import * as path from "node:path"; + +import type { + BindingClassDoc, + DirectoryCatalog, + FileDoc, + LinkDoc, + ShapeDoc, + SourceEntry, +} from "./types.ts"; +import { + docsRoot, + escapeMarkdown, + labelFromDocRelativePath, + relativeDocLink, +} from "./utils.ts"; + +function renderLinks(links: LinkDoc[]) { + return links.map((link) => `- [${link.label}](${link.href})`).join("\n"); +} + +function renderShape( + shape: ShapeDoc | undefined, + heading: string, + level: "##" | "###" = "##", +) { + if (!shape) { + return ""; + } + + if (shape.properties.length === 0) { + return [ + `${level} ${heading}`, + shape.description ?? "", + shape.signature ? ["```ts", shape.signature, "```"].join("\n") : "", + ] + .filter(Boolean) + .join("\n\n"); + } + + const rows = shape.properties.map( + (property) => + `| \`${escapeMarkdown(property.name)}\` | \`${escapeMarkdown(property.type)}\` | ${ + property.optional ? "optional" : "required" + }${property.readonly ? ", readonly" : ""} | ${escapeMarkdown( + property.defaultValue ?? "-", + )} | ${escapeMarkdown(property.description ?? "-")} |`, + ); + + const table = [ + "| Property | Type | Flags | Default | Description |", + "| --- | --- | --- | --- | --- |", + ...rows, + ].join("\n"); + + return [`${level} ${heading}`, shape.description ?? "", table] + .filter(Boolean) + .join("\n\n"); +} + +function renderBindingGroup(heading: string, items: BindingClassDoc[]) { + if (items.length === 0) { + return ""; + } + + return [ + `### ${heading}`, + ...items.map((item) => + [ + `#### \`${item.name}\``, + item.identifier ? `Identifier: \`${item.identifier}\`` : "", + item.summary ?? "", + ] + .filter(Boolean) + .join("\n\n"), + ), + ].join("\n\n"); +} + +function renderExports(fileDoc: FileDoc) { + if (fileDoc.exports.length === 0) { + return ""; + } + + const rows = fileDoc.exports.map( + (entry) => + `| \`${escapeMarkdown(entry.name)}\` | ${entry.kind} | \`${escapeMarkdown( + entry.signature, + )}\` | ${escapeMarkdown(entry.summary ?? "-")} |`, + ); + + const details = fileDoc.exports + .map((entry) => + [ + `### \`${entry.name}\``, + entry.summary ?? "", + ["```ts", entry.signature, "```"].join("\n"), + entry.shape ? renderShape(entry.shape, entry.shape.title, "###") : "", + ] + .filter(Boolean) + .join("\n\n"), + ) + .join("\n\n"); + + return [ + "## Exports", + "| Symbol | Kind | Signature | Description |", + "| --- | --- | --- | --- |", + ...rows, + "", + details, + ].join("\n"); +} + +function renderExamples(fileDoc: FileDoc) { + if (fileDoc.examples.length === 0 && !fileDoc.autoExample) { + return ""; + } + + const sections = fileDoc.examples.flatMap((section) => [ + `### ${section.title}`, + ...section.examples.flatMap((example) => [ + `#### ${example.title}`, + example.body, + ]), + ]); + + if (fileDoc.autoExample) { + sections.push( + "### Quick Start", + `#### ${fileDoc.autoExample.title}`, + fileDoc.autoExample.body, + ); + } + + return ["## Examples", ...sections].join("\n\n"); +} + +function renderCatalog(catalog: DirectoryCatalog) { + const parts = ["## Navigation"]; + if (catalog.parent) { + parts.push(`- Parent: [${catalog.parent.label}](${catalog.parent.href})`); + } + if (catalog.siblings.length > 0) { + parts.push("- Siblings"); + parts.push(renderLinks(catalog.siblings)); + } + return parts.join("\n"); +} + +function isPrimaryReferencePage(fileDoc: FileDoc) { + return ["resource", "host", "operation", "event-source"].includes( + fileDoc.fileKind, + ); +} + +export function renderFileDoc(fileDoc: FileDoc) { + const sourceHref = relativeDocLink( + fileDoc.outputPath, + path.join(docsRoot, "..", fileDoc.sourcePath), + ); + const primaryReferencePage = isPrimaryReferencePage(fileDoc); + + return [ + "", + "", + `# ${fileDoc.title}`, + "", + fileDoc.summary, + "", + `- Source: [\`${fileDoc.sourcePath}\`](${sourceHref})`, + renderExamples(fileDoc), + fileDoc.resource && !primaryReferencePage + ? [ + "## Resource Model", + `- Resource Type: \`${fileDoc.resource.resourceType}\``, + fileDoc.resource.providerName + ? `- Provider Export: \`${fileDoc.resource.providerName}\`` + : "", + fileDoc.resource.lifecycleOperations.length > 0 + ? `- Lifecycle Operations: ${fileDoc.resource.lifecycleOperations + .map((name) => `\`${name}\``) + .join(", ")}` + : "", + ] + .filter(Boolean) + .join("\n") + : "", + renderShape(fileDoc.resource?.props, "Props"), + renderShape(fileDoc.resource?.attributes, "Attributes"), + renderShape(fileDoc.resource?.binding, "Binding Contract"), + fileDoc.operation + ? [ + "## Reference", + renderBindingGroup("Runtime Bindings", fileDoc.operation.services), + renderBindingGroup( + "Deploy-Time Policies", + fileDoc.operation.policies, + ), + fileDoc.operation.runtimeLayers.length > 0 + ? [ + "### Live Layers", + fileDoc.operation.runtimeLayers + .map((name) => `- \`${name}\``) + .join("\n"), + ].join("\n\n") + : "", + fileDoc.operation.supportedHosts.length > 0 + ? [ + "### Supported Hosts", + fileDoc.operation.supportedHosts + .map((name) => `- \`${name}\``) + .join("\n"), + ].join("\n\n") + : "", + ...fileDoc.operation.requestShapes.map((shape) => + renderShape(shape, shape.title), + ), + ] + .filter(Boolean) + .join("\n\n") + : "", + fileDoc.provider + ? [ + "## Provider Exports", + fileDoc.provider.exportedFactories + .map((name) => `- \`${name}\``) + .join("\n"), + ].join("\n\n") + : "", + fileDoc.index && fileDoc.index.reExports.length > 0 + ? [ + "## Re-Exports", + "| Export | Source |", + "| --- | --- |", + ...fileDoc.index.reExports.map((entry) => { + const source = entry.href + ? `[\`${entry.sourcePath}\`](${entry.href})` + : `\`${entry.sourcePath}\``; + return `| \`${escapeMarkdown(entry.exportName)}\` | ${source} |`; + }), + ].join("\n") + : "", + !primaryReferencePage ? renderExports(fileDoc) : "", + !primaryReferencePage && fileDoc.relatedLinks.length > 0 + ? ["## Related Files", renderLinks(fileDoc.relatedLinks)].join("\n\n") + : "", + !primaryReferencePage ? renderCatalog(fileDoc.directoryCatalog) : "", + ] + .filter((part) => part && part.trim().length > 0) + .join("\n\n"); +} + +function allDirectories(entries: SourceEntry[]) { + const directories = new Set(); + for (const entry of entries) { + let current = path.dirname(entry.relativePath); + while (true) { + directories.add(current); + if (current === ".") { + break; + } + current = path.dirname(current); + } + } + return [...directories].sort(); +} + +function hasSourceIndex(directory: string, entries: SourceEntry[]) { + return entries.some( + (entry) => entry.relativePath === path.join(directory, "index.ts"), + ); +} + +export function syntheticIndexes(entries: SourceEntry[]) { + return allDirectories(entries) + .filter((directory) => !hasSourceIndex(directory, entries)) + .map((directory) => { + const outputPath = path.join( + docsRoot, + directory === "." ? "index.md" : directory, + "index.md", + ); + const files = entries + .filter( + (entry) => + path.dirname(entry.relativePath) === directory && + path.basename(entry.relativePath) !== "index.ts", + ) + .map((entry) => ({ + label: labelFromDocRelativePath(entry.relativePath), + href: relativeDocLink(outputPath, entry.outputPath), + })) + .sort((left, right) => left.label.localeCompare(right.label)); + const childDirectories = allDirectories(entries) + .filter( + (candidate) => + candidate !== "." && path.dirname(candidate) === directory, + ) + .map((candidate) => ({ + label: path.basename(candidate), + href: relativeDocLink( + outputPath, + path.join(docsRoot, candidate, "index.md"), + ), + })) + .sort((left, right) => left.label.localeCompare(right.label)); + + const title = + directory === "." ? "API Reference" : path.basename(directory); + const content = [ + "", + "", + `# ${title}`, + "", + directory === "." + ? "Static API reference generated from `alchemy/src`." + : `Directory index for \`src/${directory}\`.`, + "", + childDirectories.length > 0 + ? ["## Directories", renderLinks(childDirectories)].join("\n\n") + : "", + files.length > 0 ? ["## Files", renderLinks(files)].join("\n\n") : "", + ] + .filter((part) => part && part.trim().length > 0) + .join("\n\n"); + + return { outputPath, content }; + }); +} diff --git a/.repos/alchemy-effect/scripts/generate-docs/types.ts b/.repos/alchemy-effect/scripts/generate-docs/types.ts new file mode 100644 index 00000000000..d2c7810a7c2 --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-docs/types.ts @@ -0,0 +1,159 @@ +export type FileKind = + | "resource" + | "host" + | "operation" + | "event-source" + | "provider" + | "index" + | "helper"; + +export type ExportKind = + | "class" + | "const" + | "enum" + | "function" + | "interface" + | "namespace" + | "type"; + +export interface SourceEntry { + relativePath: string; + outputPath: string; + sourcePath: string; +} + +export interface DuplicateGroup { + canonical: string; + ignored: string[]; +} + +export interface ExampleDoc { + title: string; + body: string; +} + +export interface ExampleSection { + title: string; + examples: ExampleDoc[]; +} + +export interface JSDocInfo { + summary?: string; + defaultValue?: string; + sections: ExampleSection[]; +} + +export interface PropertyDoc { + name: string; + type: string; + optional: boolean; + readonly: boolean; + description?: string; + defaultValue?: string; +} + +export interface ShapeDoc { + title: string; + description?: string; + properties: PropertyDoc[]; + signature?: string; +} + +export interface ExportDoc { + name: string; + kind: ExportKind; + signature: string; + summary?: string; + shape?: ShapeDoc; +} + +export interface ResourceDoc { + name: string; + resourceType: string; + props?: ShapeDoc; + attributes?: ShapeDoc; + binding?: ShapeDoc; + lifecycleOperations: string[]; + providerName?: string; +} + +export interface BindingClassDoc { + name: string; + identifier?: string; + signature: string; + summary?: string; +} + +export interface OperationDoc { + services: BindingClassDoc[]; + policies: BindingClassDoc[]; + runtimeLayers: string[]; + supportedHosts: string[]; + requestShapes: ShapeDoc[]; + usage?: { + bindParameters: Array<{ + name: string; + type: string; + optional: boolean; + rest: boolean; + }>; + invokeParameters: Array<{ + name: string; + type: string; + optional: boolean; + rest: boolean; + }>; + }; +} + +export interface ReExportDoc { + exportName: string; + sourcePath: string; + href?: string; +} + +export interface IndexDoc { + reExports: ReExportDoc[]; +} + +export interface ProviderDoc { + exportedFactories: string[]; +} + +export interface LinkDoc { + label: string; + href: string; +} + +export interface DirectoryCatalog { + parent?: LinkDoc; + siblings: LinkDoc[]; +} + +export interface FileDoc { + title: string; + fileKind: FileKind; + summary: string; + sourcePath: string; + relativePath: string; + outputPath: string; + exports: ExportDoc[]; + resource?: ResourceDoc; + operation?: OperationDoc; + index?: IndexDoc; + provider?: ProviderDoc; + examples: ExampleSection[]; + autoExample?: ExampleDoc; + relatedLinks: LinkDoc[]; + directoryCatalog: DirectoryCatalog; +} + +export const lifecycleOperationOrder = [ + "read", + "diff", + "preCreate", + "create", + "update", + "delete", + "stables", +] as const; diff --git a/.repos/alchemy-effect/scripts/generate-docs/utils.ts b/.repos/alchemy-effect/scripts/generate-docs/utils.ts new file mode 100644 index 00000000000..84be053f706 --- /dev/null +++ b/.repos/alchemy-effect/scripts/generate-docs/utils.ts @@ -0,0 +1,238 @@ +import * as path from "node:path"; + +import { Node, type JSDoc } from "ts-morph"; + +import type { + ExampleDoc, + ExampleSection, + JSDocInfo, + PropertyDoc, +} from "./types.ts"; + +export const repoRoot = path.resolve(import.meta.dir, "../.."); +export const packageRoot = path.join(repoRoot, "alchemy"); +export const srcRoot = path.join(packageRoot, "src"); +export const docsRoot = path.join(packageRoot, "docs"); +export const tsConfigPath = path.join(packageRoot, "tsconfig.json"); + +export function toPosix(filePath: string) { + return filePath.split(path.sep).join("/"); +} + +export function relativeSourcePath(filePath: string) { + return toPosix(path.relative(packageRoot, filePath)); +} + +export function titleFromRelativePath(relativePath: string) { + const baseName = path.basename(relativePath, ".ts"); + if (baseName !== "index") { + return baseName; + } + const directory = path.dirname(relativePath); + return directory === "." ? "API Reference" : path.basename(directory); +} + +export function labelFromDocRelativePath(relativePath: string) { + return titleFromRelativePath(relativePath); +} + +export function lowerPathKey(relativePath: string) { + return toPosix(relativePath).toLowerCase(); +} + +export function canonicalScore(relativePath: string) { + const baseName = path.basename(relativePath); + const uppercaseCount = [...relativePath].filter( + (char) => char >= "A" && char <= "Z", + ).length; + return ( + (baseName === "index.ts" ? 1000 : 0) + + (/^[A-Z]/.test(baseName) ? 100 : 0) + + uppercaseCount * 5 + + (relativePath === relativePath.toLowerCase() ? -20 : 0) + ); +} + +export function escapeMarkdown(value: string) { + return value.replace(/\|/g, "\\|"); +} + +export function normalizeWhitespace(value: string) { + return value.replace(/\s+/g, " ").trim(); +} + +export function formatTypeText(value: string | undefined) { + return value + ? value + .replace(/\/\*\*[\s\S]*?\*\//g, " ") + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/\/\/.*$/gm, " ") + .replace(/\s+/g, " ") + .trim() + : "unknown"; +} + +export function truncateInline(value: string, maxLength = 120) { + const normalized = formatTypeText(value); + return normalized.length <= maxLength + ? normalized + : `${normalized.slice(0, maxLength - 3)}...`; +} + +export function lowerCamel(name: string) { + return name.charAt(0).toLowerCase() + name.slice(1); +} + +export function docOutputPath(relativePath: string) { + return path.join(docsRoot, relativePath.replace(/\.ts$/, ".md")); +} + +export function relativeDocLink(fromDocPath: string, toDocPath: string) { + return toPosix(path.relative(path.dirname(fromDocPath), toDocPath)); +} + +/** + * Convert JSDoc `{@link url | label}` into standard markdown links. + * Absolute `https://alchemy.run/...` URLs are rewritten to relative + * paths so the links work in any deployment (localhost, preview, prod). + */ +function resolveJSDocLinks(text: string): string { + return text.replace( + /\{@link\s+(https?:\/\/alchemy\.run(\/[^\s|}]*))\s*\|\s*([^}]+)\}/g, + (_match, _url, pathname, label) => `[${label.trim()}](${pathname})`, + ); +} + +export function cleanDocComment(raw: string) { + const cleaned = raw + .replace(/^\/\*\*?/, "") + .replace(/\*\/$/, "") + .split("\n") + .map((line) => line.replace(/^\s*\*\s?/, "")) + .join("\n"); + return resolveJSDocLinks(cleaned); +} + +function getJsDocBlocks(node: Node) { + const getter = (node as Node & { getJsDocs?: () => JSDoc[] }).getJsDocs; + return getter ? getter.call(node) : []; +} + +export function getJSDocInfo(node: Node): JSDocInfo { + const docs = getJsDocBlocks(node); + if (docs.length === 0) { + return { sections: [] }; + } + + const clean = cleanDocComment(docs.map((doc) => doc.getText()).join("\n")); + const lines = clean.split("\n"); + const summaryLines: string[] = []; + const sections: ExampleSection[] = []; + + let defaultValue: string | undefined; + let sawTag = false; + let currentSection: ExampleSection | undefined; + let currentExample: ExampleDoc | undefined; + + const ensureSection = (title: string) => { + const section = { title, examples: [] }; + sections.push(section); + currentSection = section; + return section; + }; + + const flushExample = () => { + if (!currentExample) { + return; + } + currentExample.body = currentExample.body.trim(); + if (!currentSection) { + currentSection = ensureSection("Examples"); + } + currentSection.examples.push(currentExample); + currentExample = undefined; + }; + + for (const line of lines) { + const tag = line.trimEnd().match(/^@(\w+)\s*(.*)$/); + if (tag) { + sawTag = true; + const [, name, rest] = tag; + const value = rest.trim(); + switch (name) { + case "default": + defaultValue = value || undefined; + break; + case "section": + flushExample(); + ensureSection(value || "Examples"); + break; + case "example": + flushExample(); + currentExample = { + title: value || "Example", + body: "", + }; + break; + } + continue; + } + + if (!sawTag) { + summaryLines.push(line); + continue; + } + + if (currentExample) { + currentExample.body += `${line}\n`; + } + } + + flushExample(); + + const summary = normalizeWhitespace( + summaryLines.join("\n").replace(/```[\s\S]*?```/g, ""), + ); + + return { + summary: summary || undefined, + defaultValue, + sections, + }; +} + +export function getSummary(node: Node) { + return getJSDocInfo(node).summary; +} + +export function getDefaultValue(node: Node) { + return getJSDocInfo(node).defaultValue; +} + +export function guessExampleValue(property: PropertyDoc) { + const type = property.type; + + const literal = type.match(/"([^"]+)"|'([^']+)'/); + if (literal) { + return JSON.stringify(literal[1] ?? literal[2]); + } + if (/ScalarAttributeType/.test(type)) { + return `{ pk: "S" }`; + } + if (/Record/dev/null 2>&1 || [[ "$UPDATE" -eq 1 ]]; then + curl -fsSL https://opencode.ai/install | bash +fi + +install_brew() { + # $1: executable name + # $2: brew install argument + local exe="$1" + local formula="$2" + if ! command -v "$exe" >/dev/null 2>&1; then + brew install "$formula" + elif [[ "$UPDATE" -eq 1 ]]; then + brew upgrade "$formula" + fi +} + +install_brew rg ripgrep +install_brew localstack localstack/tap/localstack-cli +localstack auth set-token $LOCALSTACK_TOKEN 2>&1 > /dev/null \ No newline at end of file diff --git a/.repos/alchemy-effect/scripts/nuke-aws.sh b/.repos/alchemy-effect/scripts/nuke-aws.sh new file mode 100755 index 00000000000..1937d0a2f13 --- /dev/null +++ b/.repos/alchemy-effect/scripts/nuke-aws.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v aws-nuke &>/dev/null; then + echo "aws-nuke is not installed." + echo "Install with: brew install ekristen/tap/aws-nuke" + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG="$SCRIPT_DIR/../aws-nuke-config.yml" + +if grep -q '123456789012' "$CONFIG"; then + echo "ERROR: You must replace the placeholder account ID in aws-nuke-config.yml" + echo "Find your account ID with: aws sts get-caller-identity --query Account --output text" + exit 1 +fi + +EXTRA_ARGS=("$@") + +aws-nuke run \ + --config "$CONFIG" \ + --no-alias-check \ + "${EXTRA_ARGS[@]}" diff --git a/.repos/alchemy-effect/scripts/pnpm-workspace.ts b/.repos/alchemy-effect/scripts/pnpm-workspace.ts new file mode 100644 index 00000000000..f11c2f55f97 --- /dev/null +++ b/.repos/alchemy-effect/scripts/pnpm-workspace.ts @@ -0,0 +1,108 @@ +#!/usr/bin/env bun +/** + * Generate `pnpm-workspace.yaml` mirroring bun's `package.json#workspaces.*` + * fields so pnpm can resolve `catalog:` and `workspace:*` refs in this repo. + * + * `bun deploy:smoke` (and the GitHub Actions matrix `runtime: pnpm` job) + * call this script before `pnpm install` so pnpm has a coherent view of the + * workspace. The file is `.gitignore`d — never check it in. + * + * Usage: + * bun ./scripts/pnpm-workspace.ts # write + * bun ./scripts/pnpm-workspace.ts --remove # delete + */ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, ".."); +const PNPM_WORKSPACE_PATH = path.join(ROOT, "pnpm-workspace.yaml"); + +if (process.argv.includes("--remove")) { + await fs.rm(PNPM_WORKSPACE_PATH, { force: true }); + console.log(`removed ${path.relative(ROOT, PNPM_WORKSPACE_PATH)}`); + process.exit(0); +} + +const pkg = JSON.parse( + await fs.readFile(path.join(ROOT, "package.json"), "utf8"), +) as { + workspaces?: { + packages?: string[]; + catalog?: Record; + }; +}; +const packages = pkg.workspaces?.packages ?? []; +const catalog = pkg.workspaces?.catalog ?? {}; + +// Bun's `catalog:` ranges are loose (`>=4.0.0-beta.60 || >=4.0.0`) and +// rely on `bun.lock` to pin the resolved version. pnpm without a lockfile +// re-resolves each `catalog:` ref to the freshest matching version, which +// lets sibling packages drift onto different `effect@4.0.0-beta.6{2,4}` +// builds and produces "Effect<...beta.62> not assignable to Effect<...beta.64>" +// type errors in CI. Mirror bun's resolved versions into the pnpm catalog +// so both runtimes see identical trees. +const bunLockText = await fs + .readFile(path.join(ROOT, "bun.lock"), "utf8") + .catch(() => ""); +const resolveFromBunLock = (name: string): string | undefined => { + // Each top-level package entry in bun.lock looks like: + // "": ["@", ...] + // (bun.lock is JSONC with trailing commas, so we regex it.) + const escaped = name.replace(/[/@.+*?(){}[\]^$|\\-]/g, "\\$&"); + const re = new RegExp(`"${escaped}":\\s*\\["${escaped}@([^"]+)"`); + return bunLockText.match(re)?.[1]; +}; +const pinnedCatalog: Record = {}; +for (const [name, range] of Object.entries(catalog)) { + // Non-semver catalog values (URLs, file: paths, workspace:* entries from + // bun.lock for monorepo packages) must pass through unchanged — pnpm + // rejects e.g. `alchemy: workspace:packages/alchemy` in a catalog + // (ERR_PNPM_CATALOG_ENTRY_INVALID_WORKSPACE_SPEC). + if (/^(https?:|file:|git\+|workspace:|npm:)/.test(range)) { + pinnedCatalog[name] = range; + continue; + } + const pinned = resolveFromBunLock(name); + pinnedCatalog[name] = + pinned && !/^(workspace:|file:|link:)/.test(pinned) ? pinned : range; +} + +const lines: string[] = [ + "# Auto-generated by scripts/pnpm-workspace.ts — DO NOT COMMIT.", + "# Mirrors `package.json#workspaces.{packages,catalog}` so pnpm can", + "# resolve `catalog:` deps in our examples (bun keeps catalogs in", + "# `package.json#workspaces.catalog`; pnpm only reads them from this", + "# file).", + "", + // pnpm 11 fails the install with `ERR_PNPM_IGNORED_BUILDS` whenever + // a transitive dep has a postinstall script that hasn't been + // explicitly approved. Whitelist only the ones our examples actually + // need to build (workerd binary download, esbuild's optional native + // binaries, sharp's libvips, etc.). + // + // pnpm 11 renamed `onlyBuiltDependencies` (list) → `allowBuilds` + // (dict). Emit both so the file works on pnpm 10 and pnpm 11; pnpm + // silently ignores unknown keys. + "onlyBuiltDependencies:", + ...["@parcel/watcher", "esbuild", "msgpackr-extract", "sharp", "workerd"].map( + (name) => ` - ${JSON.stringify(name)}`, + ), + "", + "allowBuilds:", + ...["@parcel/watcher", "esbuild", "msgpackr-extract", "sharp", "workerd"].map( + (name) => ` ${JSON.stringify(name)}: true`, + ), + "", + "packages:", + ...packages.map((p) => ` - ${JSON.stringify(p)}`), +]; +if (Object.keys(pinnedCatalog).length > 0) { + lines.push("", "catalog:"); + for (const [name, range] of Object.entries(pinnedCatalog)) { + lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(range)}`); + } +} +lines.push(""); + +await fs.writeFile(PNPM_WORKSPACE_PATH, lines.join("\n")); +console.log(`wrote ${path.relative(ROOT, PNPM_WORKSPACE_PATH)}`); diff --git a/.repos/alchemy-effect/scripts/release/bump.ts b/.repos/alchemy-effect/scripts/release/bump.ts new file mode 100644 index 00000000000..3a31e49efac --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/bump.ts @@ -0,0 +1,290 @@ +#!/usr/bin/env bun +/** + * Compute and apply a version bump across all publishable workspace packages. + * + * Writes: packages/{alchemy,better-auth,pr-package}/package.json + * and (via `bun install`) bun.lock. + * + * Prints the chosen version to stdout. All progress messages go to stderr, + * so callers can capture the version with + * VERSION=$(bun scripts/release/bump.ts ...) + * + * Does NOT commit or push. The release workflow commits only after every + * package has been published to npm, so an interrupted publish leaves no + * orphan commit behind. + * + * Channels: + * release + * Stable release. Bumps the named semver part relative to the current + * max stable version on npm, or uses the explicit version as-is. + * + * beta [N] / alpha [N] + * Auto-incrementing pre-release. With no spec, queries npm for the + * max 2.0.0-{channel}.N across every publishable package and increments. + * Pass N explicitly to force 2.0.0-{channel}.. + * + * Resume behavior: if a prior release published some packages but not + * others, or published everything but the git tag is missing on the + * remote, we resume at that N instead of incrementing past it. + * + * tag + * Use verbatim. Intended for ad-hoc channels like + * `tag 2.0.0-experimental.1` — publish-package.ts derives a custom + * npm dist-tag from the prerelease suffix. + * + * Examples: + * bun scripts/release/bump.ts release patch + * bun scripts/release/bump.ts release 2.1.0 + * bun scripts/release/bump.ts beta + * bun scripts/release/bump.ts beta 15 + * bun scripts/release/bump.ts alpha + * bun scripts/release/bump.ts tag 2.0.0-experimental.1 + */ +import { $ } from "bun"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +const PUBLISHABLE_DIRS = ["alchemy", "better-auth", "pr-package"] as const; + +const PUBLISHABLE_NAMES = [ + "alchemy", + "@alchemy.run/better-auth", + "@alchemy.run/pr-package", +] as const; + +// Pre-release versions the beta/alpha auto-increment considers when deciding +// whether to resume. Keeps the line `2.0.0-...` since that is the series +// currently being released; change here when the stable line advances. +const CURRENT_MAJOR_MINOR_PATCH = "2.0.0"; + +type Channel = "release" | "beta" | "alpha" | "tag"; + +const CHANNELS: readonly Channel[] = ["release", "beta", "alpha", "tag"]; + +function usage(): never { + console.error( + "Usage:\n" + + " bun scripts/release/bump.ts release \n" + + " bun scripts/release/bump.ts beta [N]\n" + + " bun scripts/release/bump.ts alpha [N]\n" + + " bun scripts/release/bump.ts tag ", + ); + process.exit(1); +} + +async function fetchNpmVersions(pkg: string): Promise { + try { + const r = await fetch(`https://registry.npmjs.org/${pkg}`); + if (!r.ok) return []; + const data = (await r.json()) as { versions?: Record }; + return Object.keys(data.versions ?? {}); + } catch { + return []; + } +} + +function maxPrereleaseN( + versions: readonly string[], + channel: "beta" | "alpha", +): number { + const re = new RegExp(`^${CURRENT_MAJOR_MINOR_PATCH}-${channel}\\.(\\d+)$`); + const ns = versions + .map((v) => { + const m = v.match(re); + return m ? parseInt(m[1]!, 10) : 0; + }) + .filter((n) => n > 0); + return ns.length > 0 ? Math.max(...ns) : 0; +} + +function maxStable(versions: readonly string[]): string | null { + const stables = versions.filter((v) => /^\d+\.\d+\.\d+$/.test(v)); + if (stables.length === 0) return null; + return stables.sort(compareSemver)[stables.length - 1]!; +} + +function compareSemver(a: string, b: string): number { + const pa = a.split(".").map((n) => parseInt(n, 10)); + const pb = b.split(".").map((n) => parseInt(n, 10)); + for (let i = 0; i < 3; i++) { + if (pa[i]! !== pb[i]!) return pa[i]! - pb[i]!; + } + return 0; +} + +async function getHeadTagVersion(): Promise { + const r = await $`git describe --exact-match --tags HEAD`.nothrow().quiet(); + if (r.exitCode !== 0) return null; + const tag = r.stdout.toString().trim(); + if (!/^v\d+\.\d+\.\d+(-[\w.-]+)?$/.test(tag)) return null; + return tag.slice(1); +} + +async function remoteTagExists(tag: string): Promise { + const r = + await $`git ls-remote --exit-code --tags origin ${`refs/tags/${tag}`}` + .nothrow() + .quiet(); + return r.exitCode === 0; +} + +async function resolveRelease(spec: string | undefined): Promise { + if (!spec) { + console.error("release channel requires a spec: patch|minor|major|"); + process.exit(1); + } + if (/^\d+\.\d+\.\d+$/.test(spec)) { + console.error(`Explicit release version: ${spec}`); + return spec; + } + if (spec !== "patch" && spec !== "minor" && spec !== "major") { + console.error( + `Invalid release spec: ${spec}. Use patch|minor|major|.`, + ); + process.exit(1); + } + const versions = await fetchNpmVersions("alchemy"); + const current = maxStable(versions); + if (!current) { + console.error( + "No stable `alchemy` versions on npm; cannot bump relative to current.", + ); + process.exit(1); + } + const [maj, min, pat] = current.split(".").map((n) => parseInt(n, 10)) as [ + number, + number, + number, + ]; + const bumped = + spec === "major" + ? `${maj + 1}.0.0` + : spec === "minor" + ? `${maj}.${min + 1}.0` + : `${maj}.${min}.${pat + 1}`; + console.error(`Bumping ${spec}: ${current} → ${bumped}`); + return bumped; +} + +async function resolvePrerelease( + channel: "beta" | "alpha", + spec: string | undefined, +): Promise { + if (spec !== undefined) { + if (!/^\d+$/.test(spec)) { + console.error( + `${channel} channel spec must be an integer N (got: ${spec})`, + ); + process.exit(1); + } + const explicit = `${CURRENT_MAJOR_MINOR_PATCH}-${channel}.${spec}`; + console.error(`Explicit ${channel} version: ${explicit}`); + return explicit; + } + + console.error(`Resolving next ${channel} version from npm state...`); + const perPkgMax = await Promise.all( + PUBLISHABLE_NAMES.map(async (name) => { + const versions = await fetchNpmVersions(name); + return maxPrereleaseN(versions, channel); + }), + ); + const maxN = Math.max(0, ...perPkgMax); + const allAtMax = maxN > 0 && perPkgMax.every((n) => n === maxN); + + let nextN: number; + if (maxN === 0) { + nextN = 1; + console.error( + `No ${channel} versions on npm yet; starting at ${channel}.${nextN}`, + ); + } else if (!allAtMax) { + nextN = maxN; + console.error( + `Partial publish at ${channel}.${maxN} (per-package: ${JSON.stringify( + Object.fromEntries(PUBLISHABLE_NAMES.map((n, i) => [n, perPkgMax[i]])), + )}). Resuming at ${channel}.${nextN}.`, + ); + } else if ( + !(await remoteTagExists(`v${CURRENT_MAJOR_MINOR_PATCH}-${channel}.${maxN}`)) + ) { + nextN = maxN; + console.error( + `All packages at ${channel}.${maxN} on npm but tag v${CURRENT_MAJOR_MINOR_PATCH}-${channel}.${maxN} missing on remote. Resuming at ${channel}.${nextN}.`, + ); + } else { + nextN = maxN + 1; + console.error( + `Bumping to next ${channel}: ${CURRENT_MAJOR_MINOR_PATCH}-${channel}.${nextN}`, + ); + } + return `${CURRENT_MAJOR_MINOR_PATCH}-${channel}.${nextN}`; +} + +function resolveTag(spec: string | undefined): string { + // The tag channel is the explicit-version escape hatch. Whatever the + // caller passes is used verbatim — this is the equivalent of the old + // `version-override` input. We enforce an x.y.z- shape (no plain + // stable versions) because `tag` releases are always pre-releases; use + // the `release` channel for stable x.y.z versions. + if (!spec) { + console.error( + "tag channel requires an explicit version (e.g. 2.0.0-experimental.1)", + ); + process.exit(1); + } + if (!/^\d+\.\d+\.\d+-[\w.-]+$/.test(spec)) { + console.error( + `tag channel version must be x.y.z- (always a pre-release); got: ${spec}`, + ); + process.exit(1); + } + console.error(`Using tag version: ${spec}`); + return spec; +} + +const channel = process.argv[2] as Channel | undefined; +const spec = process.argv[3]; + +if (!channel || !CHANNELS.includes(channel)) { + usage(); +} + +let newVersion: string; + +// Durability: if HEAD is already an exact release tag (commit-then-publish +// flow committed and tagged on a previous attempt that failed before npm +// publish completed), reuse that version instead of computing a new one. +// Skipped for `tag` channel, where the explicit spec is authoritative. +const headTagVersion = channel !== "tag" ? await getHeadTagVersion() : null; +if (headTagVersion) { + console.error( + `HEAD is already tagged v${headTagVersion}; resuming with this version.`, + ); + newVersion = headTagVersion; +} else { + switch (channel) { + case "release": + newVersion = await resolveRelease(spec); + break; + case "beta": + case "alpha": + newVersion = await resolvePrerelease(channel, spec); + break; + case "tag": + newVersion = resolveTag(spec); + break; + } +} + +for (const dir of PUBLISHABLE_DIRS) { + const pkgPath = join(process.cwd(), "packages", dir, "package.json"); + const pkg = JSON.parse(await readFile(pkgPath, "utf-8")); + pkg.version = newVersion; + await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); +} + +console.error("Running bun install to refresh bun.lock workspace versions..."); +await $`bun install`.quiet(); + +console.log(newVersion); diff --git a/.repos/alchemy-effect/scripts/release/discord-body.test.ts b/.repos/alchemy-effect/scripts/release/discord-body.test.ts new file mode 100644 index 00000000000..b5f7025bb0b --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/discord-body.test.ts @@ -0,0 +1,231 @@ +import { describe, expect, test } from "bun:test"; +import type { Commit } from "changelogithub"; +import { extractTagBody, toDiscordBody } from "./discord-body.ts"; +import { renderMarkdown, type RenderConfig } from "./render.ts"; + +const baseConfig: RenderConfig = { + titles: { breakingChanges: "🚨 Breaking Changes" }, + types: { + feat: { title: "🚀 Features" }, + fix: { title: "🐞 Bug Fixes" }, + perf: { title: "🏎 Performance" }, + } as RenderConfig["types"], + capitalize: true, + emoji: true, + baseUrl: "github.com", + repo: "alchemy-run/alchemy-effect", + from: "v1", + to: "v2", +}; + +function makeCommit(input: { + type: string; + scope?: string; + description: string; + hash?: string; + pr?: string; + authors?: Array<{ login?: string; name: string }>; + isBreaking?: boolean; +}): Commit { + const references: Commit["references"] = []; + if (input.hash) references.push({ type: "hash", value: input.hash }); + if (input.pr) references.push({ type: "pull-request", value: input.pr }); + return { + message: `${input.type}${input.scope ? `(${input.scope})` : ""}: ${input.description}`, + body: "", + shortHash: (input.hash ?? "").slice(0, 7), + author: { name: "t", email: "t@t" }, + authors: [], + description: input.description, + type: input.type, + scope: input.scope ?? "", + references, + isBreaking: input.isBreaking ?? false, + resolvedAuthors: input.authors, + } as Commit; +} + +describe("toDiscordBody", () => { + test("replaces every   with a plain space", () => { + const input = + "###    🐞 Bug Fixes\n- foo  -  by @bar"; + const out = toDiscordBody(input); + expect(out).not.toContain(" "); + // The heading regex further collapses the leading whitespace run. + expect(out).toContain("### 🐞 Bug Fixes"); + // Inline ` - ` separators become ` - ` (with surrounding spaces). + expect(out).toContain("- foo - by @bar"); + }); + + test("converts ... to backticks", () => { + const input = + "- foo [(abcde)](https://example.com/commit/abcdef1)"; + expect(toDiscordBody(input)).toContain("[`(abcde)`](https://example.com"); + expect(toDiscordBody(input)).not.toMatch(/<\/?samp>/); + }); + + test("collapses indented ### headings to `### ` so Discord renders them", () => { + const input = "### 🐞 Bug Fixes"; + expect(toDiscordBody(input)).toBe("### 🐞 Bug Fixes"); + }); + + test("strips #### / ##### / ###### heading markers entirely", () => { + const input = + "#####     [View changes on GitHub](https://example.com)"; + const out = toDiscordBody(input); + expect(out.startsWith("[View changes on GitHub]")).toBe(true); + expect(out).not.toMatch(/^#{4,6}/m); + }); + + test("preserves # / ## / ### headings (Discord renders these)", () => { + const input = "# Title\n## Subtitle\n### Section"; + expect(toDiscordBody(input)).toBe("# Title\n## Subtitle\n### Section"); + }); + + test("strips stray HTML tags other than ", () => { + expect(toDiscordBody("bold and italic")).toBe( + "bold and italic", + ); + expect(toDiscordBody('link')).toBe("link"); + }); + + test("leaves plain markdown untouched", () => { + const input = "- **scope**: description [link](https://example.com)"; + expect(toDiscordBody(input)).toBe(input); + }); + + test("is idempotent", () => { + const input = + "###    🐞 Bug Fixes\n- foo  -  [(abc)](https://x/)\n##### [View](https://y/)"; + const once = toDiscordBody(input); + expect(toDiscordBody(once)).toBe(once); + }); +}); + +describe("extractTagBody", () => { + const changelog = `## v1.2.0 + +###    🚀 Features + +- Shiny new thing + +#####     [View changes on GitHub](https://x/) + +--- + +## v1.1.0 + +###    🐞 Bug Fixes + +- Old fix + +--- +`; + + test("extracts the entry body for the given tag", () => { + const body = extractTagBody(changelog, "v1.2.0"); + expect(body).toBeDefined(); + expect(body).toContain("Shiny new thing"); + expect(body).not.toContain("v1.1.0"); + expect(body).not.toContain("Old fix"); + }); + + test("returns undefined for an unknown tag", () => { + expect(extractTagBody(changelog, "v9.9.9")).toBeUndefined(); + }); + + test("falls back to next `## ` heading when no `---` separator exists", () => { + const noSep = `## v1.2.0\n\nBody line\n\n## v1.1.0\n\nOther\n`; + expect(extractTagBody(noSep, "v1.2.0")).toBe("Body line"); + }); + + test("reads to end of file for the last entry", () => { + const single = `## v1.2.0\n\nOnly body\n`; + expect(extractTagBody(single, "v1.2.0")).toBe("Only body"); + }); +}); + +describe("toDiscordBody(renderMarkdown(...))", () => { + // End-to-end coverage: whatever `render.ts` emits today must survive the + // `discord-notify.ts` cleanup pipeline without leaking HTML entities, raw + // `` tags, or `####+` heading markers into Discord. + const commits = [ + makeCommit({ + type: "feat", + scope: "aws/lambda", + description: "scope public url invokes", + hash: "abcdef1234", + pr: "#42", + authors: [{ login: "sam", name: "Sam" }], + }), + makeCommit({ + type: "fix", + scope: "aws/s3", + description: "handle presigned URLs", + hash: "1111111111", + }), + makeCommit({ + type: "fix", + scope: "aws", + description: "top-level aws fix", + hash: "2222222222", + }), + makeCommit({ + type: "fix", + scope: "Cloudflare", + description: "apply cloudflare access", + hash: "3333333333", + pr: "#160", + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + const discord = toDiscordBody(md); + + test("rendered markdown contains the intentional GitHub-flavored markup", () => { + // These MUST be present in CHANGELOG.md for GitHub Releases to render + // nicely. If someone removes them from render.ts this assertion will + // fail and force an intentional decision. + expect(md).toContain(" "); + expect(md).toContain(""); + expect(md).toContain( + "#####     [View changes on GitHub]", + ); + }); + + test("discord output has no HTML entities", () => { + expect(discord).not.toContain(" "); + expect(discord).not.toMatch(/&[a-z]+;/i); + }); + + test("discord output has no raw HTML tags", () => { + expect(discord).not.toMatch(/<\/?samp>/); + expect(discord).not.toMatch(/<[a-z][^>]*>/i); + }); + + test("discord output has no #### / ##### / ###### heading markers", () => { + const deepHeading = discord + .split("\n") + .find((line) => /^#{4,6}\s/.test(line)); + expect(deepHeading).toBeUndefined(); + }); + + test("discord output preserves the nested scope structure", () => { + expect(discord).toContain("- **aws**:"); + expect(discord).toContain(" - **lambda**: Scope public url invokes"); + expect(discord).toContain(" - **s3**: Handle presigned URLs"); + expect(discord).toContain(" - Top-level aws fix"); + }); + + test("discord output preserves commit hash links as backtick-wrapped text", () => { + expect(discord).toMatch( + /\[`\(abcde\)`\]\(https:\/\/github\.com\/alchemy-run\/alchemy-effect\/commit\/abcdef1234\)/, + ); + }); + + test("discord output preserves the `[View changes on GitHub]` link as plain text", () => { + expect(discord).toContain( + "[View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v1...v2)", + ); + }); +}); diff --git a/.repos/alchemy-effect/scripts/release/discord-body.ts b/.repos/alchemy-effect/scripts/release/discord-body.ts new file mode 100644 index 00000000000..d8f61be2980 --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/discord-body.ts @@ -0,0 +1,56 @@ +/** + * Transform a CHANGELOG.md entry body (written for GitHub Releases, which + * render HTML) into text suitable for a Discord embed description. + * + * Discord renders neither HTML entities nor the `` tag, and only + * honors `#`, `##`, and `###` headings. The CHANGELOG entries produced by + * `render.ts` / `changelogithub` use: + * + * - ` ` sequences to visually indent `###`/`#####` headings and + * author/PR separators (` - `) + * - `(hash)` to render commit-hash links in small caps + * - A trailing `#####` "View changes on GitHub" line + * + * This converts those into plain-text / markdown equivalents. + */ +export function toDiscordBody(rawBody: string): string { + return ( + rawBody + .replace(/ /g, " ") + .replace(/<\/?samp>/g, "`") + // Collapse the long indent that the GitHub-flavored headings use. + .replace(/^(#{1,6})\s+/gm, "$1 ") + // Discord only renders #, ##, ### as headings; drop deeper levels so + // lines like "##### View changes on GitHub" become plain text. + .replace(/^#{4,6}\s+/gm, "") + // Strip any other stray HTML tags just in case. + .replace(/<\/?[a-z][^>]*>/gi, "") + ); +} + +/** + * Extract the body for a single `## ` entry out of a CHANGELOG.md + * string. Stops at the `---` separator that `release-notes.ts` inserts + * between entries, or at the next `## ` heading if the separator is + * missing. Returns `undefined` if the tag isn't present. + */ +export function extractTagBody( + changelog: string, + tag: string, +): string | undefined { + const heading = `## ${tag}\n`; + const start = changelog.indexOf(heading); + if (start === -1) return undefined; + const after = changelog.slice(start + heading.length); + const sepIdx = after.indexOf("\n---\n"); + const nextHeadingIdx = after.indexOf("\n## "); + const end = + sepIdx === -1 + ? nextHeadingIdx === -1 + ? after.length + : nextHeadingIdx + : nextHeadingIdx === -1 + ? sepIdx + : Math.min(sepIdx, nextHeadingIdx); + return after.slice(0, end).trim(); +} diff --git a/.repos/alchemy-effect/scripts/release/discord-notify.ts b/.repos/alchemy-effect/scripts/release/discord-notify.ts new file mode 100644 index 00000000000..c64180dbc1b --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/discord-notify.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env bun +/** + * Post a release announcement to Discord as a single embed. The body is + * read verbatim from the CHANGELOG.md entry the release-notes step just + * wrote, so Discord matches the GitHub Release copy exactly. + * + * Reads DISCORD_WEBHOOK from the environment. Silently no-ops if unset. + * + * Usage: bun scripts/release/discord-notify.ts + */ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { extractTagBody, toDiscordBody } from "./discord-body.ts"; + +const REPO = "alchemy-run/alchemy-effect"; +// Discord embed description hard limit. +const EMBED_DESCRIPTION_LIMIT = 4096; + +const tag = process.argv[2]; +const channel = process.argv[3]; +if (!tag || !channel) { + console.error("Usage: bun scripts/release/discord-notify.ts "); + process.exit(1); +} + +const webhook = process.env.DISCORD_WEBHOOK; +if (!webhook) { + console.log("DISCORD_WEBHOOK not set, skipping Discord notification"); + process.exit(0); +} + +const changelogPath = join(process.cwd(), "CHANGELOG.md"); +const changelog = await readFile(changelogPath, "utf-8"); +const rawBody = extractTagBody(changelog, tag); +if (rawBody === undefined) { + console.error(`CHANGELOG.md has no entry for ${tag}`); + process.exit(1); +} + +const body = toDiscordBody(rawBody); + +const releaseUrl = `https://github.com/${REPO}/releases/tag/${tag}`; +const description = `${body}\n\n[Full release notes →](${releaseUrl})`; + +if (description.length > EMBED_DESCRIPTION_LIMIT) { + console.error( + `Changelog (${description.length} chars) exceeds Discord embed description limit (${EMBED_DESCRIPTION_LIMIT}). Trim the changelog or split the release.`, + ); + process.exit(1); +} + +const res = await fetch(webhook, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + embeds: [ + { + title: `${tag} (${channel}) released`, + url: releaseUrl, + description, + }, + ], + allowed_mentions: { parse: [] }, + }), +}); + +if (!res.ok) { + console.error(`Discord webhook failed: ${res.status} ${await res.text()}`); + process.exit(1); +} +console.log(`Posted Discord release notification for ${tag}`); diff --git a/.repos/alchemy-effect/scripts/release/github-release.ts b/.repos/alchemy-effect/scripts/release/github-release.ts new file mode 100644 index 00000000000..e4f586a5979 --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/github-release.ts @@ -0,0 +1,142 @@ +#!/usr/bin/env bun +/** + * Create a GitHub release for a tag, with channel-aware prerelease/latest flags. + * + * GitHub's API does not allow a release to be both `prerelease=true` and + * `latest=true`. To show an alpha/beta as "Latest" when no stable release + * exists yet, we publish it with `prerelease=false` — a masquerade. This + * script compensates on the next release: if the currently-latest release + * has a pre-release-style tag and prerelease=false, we flip it back to + * `prerelease=true` before publishing the new one. That keeps the latest + * badge where the user wants it without leaving stale "stable" markers on + * alpha/beta tags. + * + * Channel → flags on the new release: + * release prerelease=false, latest=true + * beta | alpha if any true-stable release exists: prerelease=true, latest=false + * else: prerelease=false, latest=true (masquerade) + * tag prerelease=true, latest=false (always) + * + * Usage: bun scripts/release/github-release.ts + */ +import { $ } from "bun"; +import { generate } from "changelogithub"; + +type Channel = "release" | "beta" | "alpha" | "tag"; +const CHANNELS: readonly Channel[] = ["release", "beta", "alpha", "tag"]; + +// A tag looks stable iff it's `v?X.Y.Z` with no prerelease suffix. +// A "true stable" release is one with such a tag AND prerelease=false on GH; +// a masquerading alpha/beta has prerelease=false but a non-stable-shaped tag. +function isStableTag(tag: string): boolean { + return /^v?\d+\.\d+\.\d+$/.test(tag); +} + +const tag = process.argv[2]; +const channel = process.argv[3] as Channel | undefined; +if (!tag || !channel || !CHANNELS.includes(channel)) { + console.error( + "Usage: bun scripts/release/github-release.ts ", + ); + process.exit(1); +} + +const view = await $`gh release view ${tag}`.nothrow().quiet(); +if (view.exitCode === 0) { + console.log(`Release ${tag} already exists on GitHub, skipping`); + process.exit(0); +} + +// Decide flags. +let prerelease: boolean; +let latest: boolean; +if (channel === "release") { + prerelease = false; + latest = true; +} else if (channel === "tag") { + prerelease = true; + latest = false; +} else { + // alpha/beta — latest iff no true-stable release already exists. + const list = await $`gh release list --limit 500 --json tagName,isPrerelease` + .nothrow() + .quiet(); + let hasStable = false; + if (list.exitCode === 0) { + const raw = list.stdout.toString().trim(); + const releases = raw + ? (JSON.parse(raw) as Array<{ tagName: string; isPrerelease: boolean }>) + : []; + hasStable = releases.some((r) => isStableTag(r.tagName) && !r.isPrerelease); + } + if (hasStable) { + prerelease = true; + latest = false; + } else { + prerelease = false; + latest = true; + console.log( + "No true-stable release exists; publishing this prerelease with prerelease=false so it can be marked latest.", + ); + } +} + +// If we're about to become the new latest, correct the previous masquerade. +// GH auto-removes the `latest` flag from the prior release when a new one +// claims it — but it leaves prerelease=false in place, which is the lie we +// need to undo. We must also pass --latest=false because GH's API rejects +// prerelease=true + latest=true in one go. +if (latest) { + const cur = await $`gh release view --latest --json tagName,isPrerelease` + .nothrow() + .quiet(); + if (cur.exitCode === 0) { + const raw = cur.stdout.toString().trim(); + if (raw) { + const current = JSON.parse(raw) as { + tagName: string; + isPrerelease: boolean; + }; + if ( + current.tagName !== tag && + !isStableTag(current.tagName) && + !current.isPrerelease + ) { + console.log( + `Demoting previous masquerading latest ${current.tagName}: prerelease=false → true`, + ); + await $`gh release edit ${current.tagName} --prerelease=true --latest=false`; + } + } + } +} + +const prev = await $`git describe --tags --abbrev=0 ${`${tag}^`}` + .nothrow() + .quiet(); +const from = prev.exitCode === 0 ? prev.stdout.toString().trim() : undefined; + +console.log( + `Generating release notes for ${tag}${from ? ` from ${from}` : ""}`, +); +const { md } = await generate({ + from, + to: tag, + emoji: true, + contributors: true, + repo: "alchemy-run/alchemy-effect", +}); + +const args = [ + "release", + "create", + tag, + "--title", + tag, + "--notes", + md, + `--latest=${latest ? "true" : "false"}`, +]; +if (prerelease) args.push("--prerelease"); + +await $`gh ${args}`; diff --git a/.repos/alchemy-effect/scripts/release/publish-package.ts b/.repos/alchemy-effect/scripts/release/publish-package.ts new file mode 100644 index 00000000000..13a88d480f7 --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/publish-package.ts @@ -0,0 +1,153 @@ +#!/usr/bin/env bun +/** + * Publish one workspace package to npm, idempotently. + * + * - Skips if {name}@{version} is already on the registry. + * - Rewrites `workspace:*` in dependency sections to the concrete sibling + * version read from each sibling's package.json. `bun pm pack`'s own + * substitution resolves `workspace:*` via bun.lock, which can lag behind + * a fresh version bump — in beta.12 every package shipped with its + * siblings pinned to beta.11 for that reason. Doing the rewrite here + * sidesteps the lockfile entirely. + * - Selects the npm dist-tag based on the release channel: + * release → latest + * beta|alpha → next + * tag → derived from the version's prerelease suffix (e.g. + * 2.0.0-experimental.1 → experimental-1) + * + * Usage: bun scripts/release/publish-package.ts + */ +import { $ } from "bun"; +import { + existsSync, + readdirSync, + readFileSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { join, resolve } from "node:path"; + +type DepMap = Record; +type PackageJson = { + name: string; + version: string; + dependencies?: DepMap; + devDependencies?: DepMap; + peerDependencies?: DepMap; + optionalDependencies?: DepMap; +}; + +type Channel = "release" | "beta" | "alpha" | "tag"; + +const DEP_SECTIONS = [ + "dependencies", + "devDependencies", + "peerDependencies", + "optionalDependencies", +] as const satisfies readonly (keyof PackageJson)[]; + +const CHANNELS: readonly Channel[] = ["release", "beta", "alpha", "tag"]; + +const packageArg = process.argv[2]; +const channel = process.argv[3] as Channel | undefined; +if (!packageArg || !channel || !CHANNELS.includes(channel)) { + console.error( + "Usage: bun scripts/release/publish-package.ts ", + ); + process.exit(1); +} + +const repoRoot = process.cwd(); +const packageDir = resolve(repoRoot, packageArg); +const pkgPath = join(packageDir, "package.json"); + +if (!existsSync(pkgPath)) { + console.error(`No package.json at ${pkgPath}`); + process.exit(1); +} + +const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageJson; +const { name, version } = pkg; + +console.log(`--- Publishing ${name}@${version} (channel: ${channel}) ---`); + +const existing = await $`npm view ${`${name}@${version}`} version` + .nothrow() + .quiet(); +if (existing.exitCode === 0 && existing.stdout.toString().trim().length > 0) { + console.log(`${name}@${version} already published, skipping`); + process.exit(0); +} + +const siblingVersions = new Map(); +const packagesRoot = join(repoRoot, "packages"); +for (const entry of readdirSync(packagesRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const siblingPkgPath = join(packagesRoot, entry.name, "package.json"); + if (!existsSync(siblingPkgPath)) continue; + const sibling = JSON.parse(readFileSync(siblingPkgPath, "utf-8")) as { + name?: string; + version?: string; + }; + if (sibling.name && sibling.version) { + siblingVersions.set(sibling.name, sibling.version); + } +} + +let rewrote = false; +for (const section of DEP_SECTIONS) { + const deps = pkg[section]; + if (!deps) continue; + for (const [dep, value] of Object.entries(deps)) { + if (typeof value !== "string" || !value.startsWith("workspace:")) continue; + const spec = value.slice("workspace:".length); + const concrete = siblingVersions.get(dep); + if (!concrete) { + console.error( + `${name}: ${section}.${dep} is ${value} but no workspace package provides ${dep}`, + ); + process.exit(1); + } + const rewritten = + spec === "*" || spec === "" + ? concrete + : spec === "^" + ? `^${concrete}` + : spec === "~" + ? `~${concrete}` + : spec; + deps[dep] = rewritten; + console.log(` ${section}.${dep}: ${value} → ${rewritten}`); + rewrote = true; + } +} + +if (rewrote) { + writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); +} + +const $pkg = $.cwd(packageDir); +await $pkg`bun pm pack --destination .`; + +const tarballs = readdirSync(packageDir).filter((f) => f.endsWith(".tgz")); +if (tarballs.length !== 1) { + console.error( + `Expected exactly one .tgz in ${packageDir}, found ${tarballs.length}: ${tarballs.join(", ")}`, + ); + process.exit(1); +} +const tarball = tarballs[0]!; + +const distTag = + channel === "release" + ? "latest" + : channel === "beta" || channel === "alpha" + ? "next" + : version.replace(/^\d+\.\d+\.\d+-/, "").replace(/\./g, "-"); +console.log(`Publishing tarball: ${tarball} (dist-tag: ${distTag})`); + +await $pkg`npm publish ${tarball} --access public --tag ${distTag}`; + +unlinkSync(join(packageDir, tarball)); + +console.log(`--- Published ${name}@${version} ---`); diff --git a/.repos/alchemy-effect/scripts/release/release-notes.ts b/.repos/alchemy-effect/scripts/release/release-notes.ts new file mode 100644 index 00000000000..e26696948e1 --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/release-notes.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env bun +/** + * Prepend release notes for a tag to CHANGELOG.md. Idempotent: if the tag + * already appears as a heading in CHANGELOG.md, does nothing. + * + * Usage: bun scripts/release/release-notes.ts v2.0.0-beta.13 + * + * Uses `changelogithub` to parse commits and resolve authors, then renders + * markdown with `./render.ts` so scopes containing `/` (e.g. + * `fix(aws/lambda): ...`) nest hierarchically instead of becoming flat + * unrelated groups. + */ +import { $ } from "bun"; +import { generate } from "changelogithub"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { renderMarkdown } from "./render.ts"; + +const tag = process.argv[2]; +if (!tag) { + console.error("Usage: bun scripts/release/release-notes.ts "); + process.exit(1); +} + +const changelogPath = join(process.cwd(), "CHANGELOG.md"); +const existing = await readFile(changelogPath, "utf-8"); +if (existing.includes(`## ${tag}\n`)) { + console.log(`${tag} already in CHANGELOG.md, skipping`); + process.exit(0); +} + +// changelogithub uses `to` as a git revision in `git log ...`. +// In the commit-then-tag flow this script runs BEFORE the tag is created, +// so resolve the revision to HEAD while keeping the tag string for the +// markdown heading. If the tag already exists locally (resumed run), use +// it so the diff is stable. +const tagExists = + (await $`git rev-parse --verify ${`refs/tags/${tag}`}`.nothrow().quiet()) + .exitCode === 0; +const toRev = tagExists ? tag : "HEAD"; + +console.log(`Generating release notes for ${tag} (using ${toRev})`); +const { commits, config } = await generate({ + to: toRev, + emoji: true, + contributors: true, + repo: "alchemy-run/alchemy-effect", +}); + +const md = renderMarkdown(commits, config); + +await writeFile(changelogPath, `## ${tag}\n\n${md}\n\n---\n\n${existing}`); diff --git a/.repos/alchemy-effect/scripts/release/render-parity.test.ts b/.repos/alchemy-effect/scripts/release/render-parity.test.ts new file mode 100644 index 00000000000..d5ad0121030 --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/render-parity.test.ts @@ -0,0 +1,163 @@ +/** + * Parity test: for commits whose scopes contain no `/`, `renderMarkdown` + * must produce byte-identical output to `changelogithub.generateMarkdown`. + * This guards against accidentally regressing the existing release-notes + * format for all historical content that predates `/`-style scopes. + */ +import { describe, expect, test } from "bun:test"; +import { generateMarkdown, type Commit } from "changelogithub"; +import { renderMarkdown, type RenderConfig } from "./render.ts"; + +const config = { + titles: { breakingChanges: "🚨 Breaking Changes" }, + types: { + feat: { title: "🚀 Features" }, + fix: { title: "🐞 Bug Fixes" }, + perf: { title: "🏎 Performance" }, + }, + capitalize: true, + emoji: true, + baseUrl: "github.com", + repo: "alchemy-run/alchemy-effect", + from: "v1", + to: "v2", + group: true, + scopeMap: {}, + contributors: true, + tag: "v%s", +} as unknown as RenderConfig & Parameters[1]; + +function makeCommit(input: { + type: string; + scope?: string; + description: string; + hash?: string; + pr?: string; + authors?: Array<{ login?: string; name: string }>; + isBreaking?: boolean; +}): Commit { + const references: Commit["references"] = []; + if (input.hash) references.push({ type: "hash", value: input.hash }); + if (input.pr) references.push({ type: "pull-request", value: input.pr }); + return { + message: `${input.type}${input.scope ? `(${input.scope})` : ""}: ${input.description}`, + body: "", + shortHash: (input.hash ?? "").slice(0, 7), + author: { name: "t", email: "t@t" }, + authors: [], + description: input.description, + type: input.type, + scope: input.scope ?? "", + references, + isBreaking: input.isBreaking ?? false, + resolvedAuthors: input.authors, + } as Commit; +} + +describe("render.ts / changelogithub parity for non-`/` scopes", () => { + test("single commit, single scope", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "Cloudflare", + description: "apply access headers", + hash: "fd329e70", + pr: "#160", + authors: [{ login: "jj", name: "JJ" }], + }), + ]; + expect(renderMarkdown(commits, config)).toBe( + generateMarkdown(commits, config), + ); + }); + + test("multiple commits sharing a single-segment scope", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "core", + description: "first", + hash: "1111111", + }), + makeCommit({ + type: "fix", + scope: "core", + description: "second", + hash: "2222222", + }), + makeCommit({ + type: "fix", + scope: "core", + description: "third", + hash: "3333333", + }), + ]; + expect(renderMarkdown(commits, config)).toBe( + generateMarkdown(commits, config), + ); + }); + + test("mixed types, scopes, unscoped, and breaking changes", () => { + const commits = [ + makeCommit({ + type: "feat", + description: "unscoped feature", + hash: "aaa0000", + }), + makeCommit({ + type: "feat", + scope: "api", + description: "new endpoint", + hash: "bbb0000", + }), + makeCommit({ + type: "fix", + scope: "core", + description: "core fix one", + hash: "ccc0000", + pr: "#100", + }), + makeCommit({ + type: "fix", + scope: "core", + description: "core fix two", + hash: "ddd0000", + }), + makeCommit({ + type: "fix", + scope: "website", + description: "solo website fix", + hash: "eee0000", + authors: [{ login: "alice", name: "Alice" }, { name: "Bob" }], + }), + makeCommit({ + type: "feat", + scope: "api", + description: "breaking api change", + hash: "fff0000", + isBreaking: true, + }), + ]; + expect(renderMarkdown(commits, config)).toBe( + generateMarkdown(commits, config), + ); + }); + + test("empty commit list", () => { + expect(renderMarkdown([], config)).toBe(generateMarkdown([], config)); + }); + + test("perf type section", () => { + const commits = [ + makeCommit({ + type: "perf", + scope: "runtime", + description: "faster", + hash: "ppp0000", + }), + ]; + expect(renderMarkdown(commits, config)).toBe( + generateMarkdown(commits, config), + ); + }); +}); diff --git a/.repos/alchemy-effect/scripts/release/render.test.ts b/.repos/alchemy-effect/scripts/release/render.test.ts new file mode 100644 index 00000000000..d7ed969f6f4 --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/render.test.ts @@ -0,0 +1,357 @@ +import { describe, expect, test } from "bun:test"; +import type { Commit } from "changelogithub"; +import { renderMarkdown, type RenderConfig } from "./render.ts"; + +// Minimal config that matches the defaults used by `release-notes.ts`. +const baseConfig: RenderConfig = { + titles: { breakingChanges: "🚨 Breaking Changes" }, + types: { + feat: { title: "🚀 Features" }, + fix: { title: "🐞 Bug Fixes" }, + perf: { title: "🏎 Performance" }, + } as RenderConfig["types"], + capitalize: true, + emoji: true, + baseUrl: "github.com", + repo: "alchemy-run/alchemy-effect", + from: "v1", + to: "v2", +}; + +function makeCommit(input: { + type: string; + scope?: string; + description: string; + hash?: string; + pr?: string; + authors?: Array<{ login?: string; name: string }>; + isBreaking?: boolean; +}): Commit { + const references: Commit["references"] = []; + if (input.hash) references.push({ type: "hash", value: input.hash }); + if (input.pr) references.push({ type: "pull-request", value: input.pr }); + return { + message: `${input.type}${input.scope ? `(${input.scope})` : ""}: ${input.description}`, + body: "", + shortHash: (input.hash ?? "").slice(0, 7), + author: { name: "t", email: "t@t" }, + authors: [], + description: input.description, + type: input.type, + scope: input.scope ?? "", + references, + isBreaking: input.isBreaking ?? false, + resolvedAuthors: input.authors, + } as Commit; +} + +const COMPARE_LINE = + "#####     [View changes on GitHub](https://github.com/alchemy-run/alchemy-effect/compare/v1...v2)"; + +describe("renderMarkdown", () => { + test("renders empty when no commits", () => { + const md = renderMarkdown([], baseConfig); + expect(md).toBe(`*No significant changes*\n\n${COMPARE_LINE}`); + }); + + test("nests commits that share a top-level scope via `/`", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "aws/lambda", + description: "scope public url invokes", + hash: "abcdef123456", + }), + makeCommit({ + type: "fix", + scope: "aws/lambda", + description: "second lambda fix", + hash: "1111111111", + }), + makeCommit({ + type: "fix", + scope: "aws/s3", + description: "handle presigned URLs", + hash: "2222222222", + }), + makeCommit({ + type: "fix", + scope: "aws", + description: "top-level aws fix", + hash: "3333333333", + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + + // Single shared `aws` header for all four commits. + expect(md.match(/- \*\*aws\*\*:/g) ?? []).toHaveLength(1); + // `lambda` has two commits -> nested list with header. + expect(md).toContain(" - **lambda**:\n"); + expect(md).toContain(" - Second lambda fix"); + expect(md).toContain(" - Scope public url invokes"); + // `s3` also emits a header + nested bullet (matches changelogithub's + // `group: true` default: if any sibling has >1 commits, every sibling + // gets a header for visual alignment). + expect(md).toContain(" - **s3**:\n - Handle presigned URLs"); + // Plain `fix(aws)` shows as a bare bullet under the aws header. + expect(md).toContain(" - Top-level aws fix"); + // No stray flat `**aws/lambda**` or `**aws/s3**` groups. + expect(md).not.toContain("**aws/lambda**"); + expect(md).not.toContain("**aws/s3**"); + }); + + test("keeps single-segment scopes flat (byte-compatible with changelogithub)", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "Cloudflare", + description: "apply Cloudflare Access headers to HTTP requests", + hash: "fd329e70aaaa", + pr: "#160", + authors: [{ login: "jacobiajohnson", name: "jj" }], + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + + expect(md).toContain( + "- **Cloudflare**: Apply Cloudflare Access headers to HTTP requests  -  by @jacobiajohnson in https://github.com/alchemy-run/alchemy-effect/issues/160 [(fd329)](https://github.com/alchemy-run/alchemy-effect/commit/fd329e70aaaa)", + ); + expect(md).not.toContain("- **Cloudflare**:\n"); + }); + + test("nests multi-commit single-segment scopes with a header", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "Cloudflare", + description: "first", + hash: "aaaaaaa", + }), + makeCommit({ + type: "fix", + scope: "Cloudflare", + description: "second", + hash: "bbbbbbb", + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + + expect(md).toContain("- **Cloudflare**:\n"); + expect(md).toMatch(/- \*\*Cloudflare\*\*:\n\s+- Second[\s\S]+- First/); + }); + + test("renders unscoped commits as plain bullets at the top of their type", () => { + const commits = [ + makeCommit({ type: "fix", description: "no scope", hash: "abcabca" }), + makeCommit({ + type: "fix", + scope: "core", + description: "core thing", + hash: "deadbee", + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + + const bugFixesBlock = md.slice(md.indexOf("🐞 Bug Fixes")); + const noScopeIdx = bugFixesBlock.indexOf("- No scope"); + const coreIdx = bugFixesBlock.indexOf("- **core**:"); + expect(noScopeIdx).toBeGreaterThan(-1); + expect(coreIdx).toBeGreaterThan(-1); + expect(noScopeIdx).toBeLessThan(coreIdx); + }); + + test("groups types under their titles and partitions breaking changes", () => { + const commits = [ + makeCommit({ + type: "feat", + scope: "api", + description: "new endpoint", + hash: "feat0000", + }), + makeCommit({ + type: "fix", + scope: "api", + description: "bug", + hash: "fix00000", + }), + makeCommit({ + type: "feat", + scope: "api", + description: "breaking change", + hash: "brk00000", + isBreaking: true, + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + + const breakingIdx = md.indexOf("🚨 Breaking Changes"); + const featuresIdx = md.indexOf("🚀 Features"); + const fixesIdx = md.indexOf("🐞 Bug Fixes"); + expect(breakingIdx).toBeGreaterThan(-1); + expect(featuresIdx).toBeGreaterThan(breakingIdx); + expect(fixesIdx).toBeGreaterThan(featuresIdx); + // Breaking change must not also appear under Features. + const featuresBlock = md.slice(featuresIdx, fixesIdx); + expect(featuresBlock).not.toContain("Breaking change"); + }); + + test("honors scopeMap before splitting on `/`", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "aws-lambda", + description: "mapped", + hash: "map00000", + }), + ]; + + const md = renderMarkdown(commits, { + ...baseConfig, + scopeMap: { "aws-lambda": "aws/lambda" }, + }); + + expect(md).toContain("- **aws**:"); + expect(md).toContain(" - **lambda**: Mapped"); + }); + + test("splits scopes with leading/trailing whitespace and empty segments", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: " aws / lambda ", + description: "trims segments", + hash: "trim0000", + }), + makeCommit({ + type: "fix", + scope: "aws//s3", + description: "skips empty segments", + hash: "skip0000", + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + expect(md).toContain("- **aws**:"); + // Both children are single-commit leaves with no other siblings forcing + // a header, so they collapse inline. + expect(md).toContain(" - **lambda**: Trims segments"); + expect(md).toContain(" - **s3**: Skips empty segments"); + }); + + test("collapses single-commit scopes inline when no sibling forces a header", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "core", + description: "just one", + hash: "core0000", + }), + makeCommit({ + type: "fix", + scope: "website", + description: "also one", + hash: "web00000", + }), + ]; + const md = renderMarkdown(commits, baseConfig); + expect(md).toContain("- **core**: Just one"); + expect(md).toContain("- **website**: Also one"); + expect(md).not.toContain("- **core**:\n"); + }); + + test("forces headers on all siblings when any has multiple commits", () => { + // `group: true` default behavior: `website` is a single-commit leaf but + // gets a header because `core` has multiple commits in the same section. + const commits = [ + makeCommit({ + type: "fix", + scope: "core", + description: "first core", + hash: "c1c1c1c", + }), + makeCommit({ + type: "fix", + scope: "core", + description: "second core", + hash: "c2c2c2c", + }), + makeCommit({ + type: "fix", + scope: "website", + description: "solo website", + hash: "w1w1w1w", + }), + ]; + const md = renderMarkdown(commits, baseConfig); + expect(md).toContain("- **core**:\n"); + expect(md).toContain("- **website**:\n"); + expect(md).not.toContain("- **website**: Solo website"); + }); + + test("renders three-level nesting", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "aws/lambda/urls", + description: "first", + hash: "aaa0000", + }), + makeCommit({ + type: "fix", + scope: "aws/lambda/urls", + description: "second", + hash: "bbb0000", + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + + expect(md).toContain("- **aws**:"); + expect(md).toContain(" - **lambda**:"); + expect(md).toContain(" - **urls**:"); + expect(md).toContain(" - First"); + expect(md).toContain(" - Second"); + }); + + test("formats multiple authors with `and`", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "core", + description: "thing", + hash: "a1b2c3d", + authors: [{ login: "alice", name: "Alice" }, { name: "Bob" }], + }), + ]; + + const md = renderMarkdown(commits, baseConfig); + expect(md).toContain("by @alice and **Bob**"); + }); + + test("strips emojis from section titles when emoji=false", () => { + const commits = [ + makeCommit({ type: "fix", description: "x", hash: "1234567" }), + ]; + const md = renderMarkdown(commits, { ...baseConfig, emoji: false }); + expect(md).toContain("###    Bug Fixes"); + expect(md).not.toContain("🐞"); + }); + + test("respects capitalize=false", () => { + const commits = [ + makeCommit({ + type: "fix", + scope: "core", + description: "lowercase description", + hash: "1234567", + }), + ]; + const md = renderMarkdown(commits, { ...baseConfig, capitalize: false }); + expect(md).toContain("- **core**: lowercase description"); + }); +}); diff --git a/.repos/alchemy-effect/scripts/release/render.ts b/.repos/alchemy-effect/scripts/release/render.ts new file mode 100644 index 00000000000..20b055eabe6 --- /dev/null +++ b/.repos/alchemy-effect/scripts/release/render.ts @@ -0,0 +1,239 @@ +/** + * Custom markdown renderer for changelogithub-parsed commits. + * + * Mirrors the output of changelogithub's built-in generator but groups + * conventional-commit scopes hierarchically by splitting on `/`. For + * example commits like `fix(aws/lambda): ...`, `fix(aws/s3): ...` and + * `fix(aws): ...` render as nested categories under a shared `**aws**` + * top-level scope rather than as three unrelated flat groups. + * + * Line-level formatting (authors, PR/issue links, `` hash tags, + * ` ` spacing) is kept byte-compatible with changelogithub so output + * for single-segment scopes is identical to the upstream renderer. + */ +import type { ChangelogOptions, Commit } from "changelogithub"; + +export type RenderConfig = Required< + Pick< + ChangelogOptions, + | "titles" + | "types" + | "capitalize" + | "emoji" + | "baseUrl" + | "repo" + | "from" + | "to" + > +> & { + scopeMap?: Record; +}; + +const emojisRE = + /([\u2700-\u27BF\uE000-\uF8FF\u2011-\u26FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|\uD83E[\uDD10-\uDDFF])/g; + +export function renderMarkdown( + commits: Commit[], + config: RenderConfig, +): string { + const lines: string[] = []; + const [breaking, rest] = partition(commits, (c) => c.isBreaking); + + if (config.titles?.breakingChanges) { + lines.push( + ...renderSection(breaking, config.titles.breakingChanges, config), + ); + } + + const byType = groupBy(rest, (c) => c.type); + for (const type of Object.keys(config.types)) { + const items = byType[type] || []; + lines.push(...renderSection(items, config.types[type].title, config)); + } + + if (!lines.length) lines.push("*No significant changes*"); + + const url = `https://${config.baseUrl}/${config.repo}/compare/${config.from}...${config.to}`; + lines.push( + "", + `#####     [View changes on GitHub](${url})`, + ); + + return lines.join("\n").trim(); +} + +interface Node { + commits: Commit[]; + children: Record; +} + +function makeNode(): Node { + return { commits: [], children: {} }; +} + +function renderSection( + commits: Commit[], + sectionName: string, + config: RenderConfig, +): string[] { + if (!commits.length) return []; + const out: string[] = ["", formatTitle(sectionName, config), ""]; + + // Build a tree keyed by the `/`-separated scope segments. Commits attach + // to the deepest node that matches their scope exactly. + const root = makeNode(); + for (const commit of commits) { + const scope = (commit.scope ?? "").trim(); + const mapped = config.scopeMap?.[scope] ?? scope; + const segments = mapped + ? mapped + .split("/") + .map((s) => s.trim()) + .filter(Boolean) + : []; + let node = root; + for (const seg of segments) { + node.children[seg] ??= makeNode(); + node = node.children[seg]; + } + node.commits.push(commit); + } + + out.push(...renderNode(root, 0, config)); + return out; +} + +function renderNode(node: Node, depth: number, config: RenderConfig): string[] { + const lines: string[] = []; + const pad = " ".repeat(depth); + + // Reverse to match changelogithub's ordering: commits come out of git log + // newest-first, and the upstream renderer flips them so the oldest entry + // in the range appears at the top of each bucket. + for (const commit of [...node.commits].reverse()) { + lines.push(`${pad}- ${formatLine(commit, config)}`); + } + + const childNames = Object.keys(node.children).sort(); + + // Match changelogithub's `group: true` default: within a given level, if + // ANY sibling scope has more than a single inline-able commit (either + // multiple commits or further sub-scopes), force every sibling to render + // with a header. This keeps visual alignment consistent and is what the + // existing CHANGELOG.md entries already use, so it avoids regressing + // historical release-notes style. + const forceHeaders = childNames.some((name) => { + const child = node.children[name]; + return countCommits(child) > 1 || Object.keys(child.children).length > 0; + }); + + for (const name of childNames) { + const child = node.children[name]; + const label = `**${name}**`; + + const childCommitCount = countCommits(child); + const hasGrandchildren = Object.keys(child.children).length > 0; + + // Collapse inline only when NO sibling wants a header and this child is + // a single-commit leaf. Otherwise emit the header + nested bullets. + if (!forceHeaders && childCommitCount === 1 && !hasGrandchildren) { + const [commit] = child.commits; + lines.push(`${pad}- ${label}: ${formatLine(commit, config)}`); + continue; + } + + lines.push(`${pad}- ${label}:`); + lines.push(...renderNode(child, depth + 1, config)); + } + + return lines; +} + +function countCommits(node: Node): number { + let n = node.commits.length; + for (const child of Object.values(node.children)) n += countCommits(child); + return n; +} + +function formatTitle(name: string, config: RenderConfig): string { + if (!config.emoji) name = name.replace(emojisRE, ""); + return `###    ${name.trim()}`; +} + +function formatLine(commit: Commit, config: RenderConfig): string { + const prRefs = formatReferences(commit.references, config, "issues"); + const hashRefs = formatReferences(commit.references, config, "hash"); + const authorNames = [ + ...new Set( + (commit.resolvedAuthors ?? []).map((a) => + a.login ? `@${a.login}` : `**${a.name}**`, + ), + ), + ]; + const authors = joinWithAnd(authorNames).trim(); + const authorStr = authors ? `by ${authors}` : ""; + let refs = [authorStr, prRefs, hashRefs] + .filter((s) => s && s.trim()) + .join(" "); + if (refs) refs = ` -  ${refs}`; + const description = config.capitalize + ? capitalize(commit.description) + : commit.description; + return [description, refs].filter((s) => s && s.trim()).join(" "); +} + +function formatReferences( + references: Commit["references"], + config: RenderConfig, + kind: "issues" | "hash", +): string { + const baseUrl = config.baseUrl; + const repo = config.repo; + const refs = references + .filter((r) => + kind === "issues" + ? r.type === "issue" || r.type === "pull-request" + : r.type === "hash", + ) + .map((ref) => { + if (!repo) return ref.value; + if (ref.type === "pull-request" || ref.type === "issue") { + return `https://${baseUrl}/${repo}/issues/${ref.value.slice(1)}`; + } + return `[(${ref.value.slice(0, 5)})](https://${baseUrl}/${repo}/commit/${ref.value})`; + }); + const joined = joinWithAnd(refs).trim(); + if (kind === "issues") return joined ? `in ${joined}` : ""; + return joined; +} + +function capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function joinWithAnd( + array: string[], + glue = ", ", + finalGlue = " and ", +): string { + if (!array || array.length === 0) return ""; + if (array.length === 1) return array[0]; + if (array.length === 2) return array.join(finalGlue); + return `${array.slice(0, -1).join(glue)}${finalGlue}${array.slice(-1)}`; +} + +function partition(items: T[], pred: (item: T) => boolean): [T[], T[]] { + const yes: T[] = []; + const no: T[] = []; + for (const item of items) (pred(item) ? yes : no).push(item); + return [yes, no]; +} + +function groupBy(items: T[], key: (item: T) => string): Record { + const out: Record = {}; + for (const item of items) { + const k = key(item); + (out[k] ??= []).push(item); + } + return out; +} diff --git a/.repos/alchemy-effect/scripts/test.ts b/.repos/alchemy-effect/scripts/test.ts new file mode 100644 index 00000000000..c5b822abc2b --- /dev/null +++ b/.repos/alchemy-effect/scripts/test.ts @@ -0,0 +1,53 @@ +import { $ } from "bun"; +import * as net from "net"; + +const isLocal = !!process.env.LOCAL; + +if (isLocal) { + await $`rm -rf .alchemy`.quiet(); + + // Start localstack in detached mode (quiet - no stdout/stderr) + await $`pkill localstack || true`.quiet(); + await $`localstack start -d`.quiet(); + + // Wait for localstack port 4566 to be open before proceeding + while (true) { + // INSERT_YOUR_CODE + // Try to connect with node to 127.0.0.1:$1 until successful. + try { + await new Promise((resolve, reject) => { + const socket = net.connect({ port: 4566, host: "127.0.0.1" }, () => { + socket.end(); + resolve(true); + }); + socket.on("error", () => { + reject(new Error("Failed to connect to localstack")); + }); + }); + break; + } catch {} + } + await new Promise((resolve) => setTimeout(resolve, 1000)); +} + +let exitCode = 1; + +try { + // Run tests with inherited stdout/stderr, passing through all args + const args = process.argv.slice(2); + const proc = Bun.spawn(["bun", "vitest", "run", ...args], { + stdio: ["inherit", "inherit", "inherit"], + env: process.env, + cwd: process.cwd(), + }); + + // Wait for the process to complete + exitCode = await proc.exited; +} finally { + if (isLocal) { + // Stop localstack as cleanup + await $`localstack stop`.quiet(); + } +} + +process.exit(exitCode); diff --git a/.repos/alchemy-effect/stacks/github.ts b/.repos/alchemy-effect/stacks/github.ts new file mode 100644 index 00000000000..f7ed5bbdc54 --- /dev/null +++ b/.repos/alchemy-effect/stacks/github.ts @@ -0,0 +1,169 @@ +import * as Alchemy from "alchemy"; +import * as AWS from "alchemy/AWS"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as GitHub from "alchemy/GitHub"; +import * as Neon from "alchemy/Neon"; +import * as Output from "alchemy/Output"; +import * as Config from "effect/Config"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; + +const REPO = { owner: "alchemy-run", repository: "alchemy-effect" } as const; + +export default Alchemy.Stack( + "AlchemyGitHubSecrets", + { + providers: Layer.mergeAll( + AWS.providers(), + Cloudflare.providers(), + GitHub.providers(), + Neon.providers(), + ), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const testAccountId = yield* Config.string("TEST_CLOUDFLARE_ACCOUNT_ID"); + const prodAccountId = yield* Config.string("PROD_CLOUDFLARE_ACCOUNT_ID"); + const discordWebhookUrl = yield* Config.string("DISCORD_WEBHOOK_URL"); + const prPackageAuthToken = yield* Config.string("PR_PACKAGE_TOKEN"); + + const testApiToken = yield* token("TestApiToken", { + accountId: testAccountId, + }); + + const prodApiToken = yield* token("ProdApiToken", { + accountId: prodAccountId, + }); + + // GitHub OIDC trust for AWS — lets `.github/workflows/test.yml` (and any + // future workflow) assume an IAM role via `aws-actions/configure-aws-credentials` + // with no long-lived AWS_ACCESS_KEY_ID secrets in the repo. + const oidc = yield* AWS.IAM.OpenIDConnectProvider("GitHubOidc", { + url: "https://token.actions.githubusercontent.com", + clientIDList: ["sts.amazonaws.com"], + // GitHub's well-known OIDC thumbprint. AWS auto-discovers thumbprints + // for github.com these days, but our `iam.updateOpenIDConnectProviderThumbprint` + // sync still requires a non-empty list when comparing against the + // cloud-observed value. + // https://aws.amazon.com/blogs/security/use-iam-roles-to-connect-github-actions-to-actions-in-aws/ + thumbprintList: ["6938fd4d98bab03faadb97b34396831e3780aea1"], + }); + + const role = yield* AWS.IAM.Role("GitHubActionsRole", { + roleName: "alchemy-github-actions", + assumeRolePolicyDocument: { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { + Federated: oidc.openIDConnectProviderArn, + }, + Action: ["sts:AssumeRoleWithWebIdentity"], + Condition: { + StringEquals: { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", + }, + // Restrict to any branch / PR / tag inside this repo. Tighten + // further (e.g. `repo:.../environment:prod`) once we wire up + // GitHub Environments. + StringLike: { + "token.actions.githubusercontent.com:sub": `repo:${REPO.owner}/${REPO.repository}:*`, + }, + }, + }, + ], + }, + // The smoke suite deploys real Cloudflare workers, AWS Lambdas, S3 + // buckets, DynamoDB tables, etc., so it needs broad access. Swap for + // a custom-managed policy enumerating `lambda:*`, `dynamodb:*`, … if + // you want least-privilege CI. + managedPolicyArns: ["arn:aws:iam::aws:policy/AdministratorAccess"], + }); + + const region = yield* AWS.Region; + + yield* GitHub.Secrets({ + ...REPO, + secrets: { + TEST_CLOUDFLARE_API_TOKEN: testApiToken.value, + TEST_CLOUDFLARE_ACCOUNT_ID: testAccountId, + PROD_CLOUDFLARE_API_TOKEN: prodApiToken.value, + PROD_CLOUDFLARE_ACCOUNT_ID: prodAccountId, + DISCORD_WEBHOOK_URL: discordWebhookUrl, + PR_PACKAGE_TOKEN: prPackageAuthToken, + NEON_API_KEY: (yield* Neon.NeonEnvironment).apiKey, + }, + }); + + // Role ARN + region are not secret — publish as repo-level Variables + // so workflows can reference `vars.AWS_ROLE_ARN` / `vars.AWS_REGION`. + yield* GitHub.Variables({ + ...REPO, + variables: { + AWS_ROLE_ARN: role.roleArn, + AWS_REGION: region, + }, + }); + + return { + TEST_CLOUDFLARE_API_TOKEN: testApiToken.value.pipe( + Output.map(Redacted.value), + ), + TEST_CLOUDFLARE_ACCOUNT_ID: testAccountId, + PROD_CLOUDFLARE_API_TOKEN: prodApiToken.value.pipe( + Output.map(Redacted.value), + ), + PROD_CLOUDFLARE_ACCOUNT_ID: prodAccountId, + DISCORD_WEBHOOK_URL: discordWebhookUrl, + AWS_ROLE_ARN: role.roleArn, + AWS_REGION: region, + }; + }).pipe(Effect.orDie), +); + +const token = ( + id: string, + props: { + accountId: string; + }, +) => + Cloudflare.AccountApiToken(id, { + accountId: props.accountId, + policies: [ + { + effect: "allow", + permissionGroups: [ + // Worker / runtime data plane + "Workers Scripts Write", + "Workers KV Storage Write", + "Workers R2 Storage Write", + "Workers Routes Write", + "Workers Tail Read", + "Workers Observability Write", + // Storage / data services + "D1 Write", + "Queues Write", + "Hyperdrive Write", + "Pipelines Write", + "Vectorize Write", + // Higher-level Worker features used by examples + "AI Gateway Write", + // Containers + "Workers Containers Write", + "Cloudchamber Write", + "Browser Rendering Write", + // Static assets / sites + "Pages Write", + // Misc + "Account Settings Write", + "Secrets Store Write", + "Logs Write", + ], + resources: { + [`com.cloudflare.api.account.${props.accountId}`]: "*", + }, + }, + ], + }); diff --git a/.repos/alchemy-effect/stacks/otel.ts b/.repos/alchemy-effect/stacks/otel.ts new file mode 100644 index 00000000000..682e6840f6f --- /dev/null +++ b/.repos/alchemy-effect/stacks/otel.ts @@ -0,0 +1,98 @@ +import * as Alchemy from "alchemy"; +import * as Axiom from "alchemy/Axiom"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as GitHub from "alchemy/GitHub"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +import { CliOverviewDashboard } from "./otel/Dashboard.ts"; +import { Logs, Metrics, Traces } from "./otel/Datasets.ts"; +import Ingester from "./otel/Ingester.ts"; +import { + ActiveUsersByCi, + ActiveUsersByVersion, + ActiveUsersHourly, + CliInvocations, + DeployDestroyLatency, + ResourceErrorRate, + ResourceLatency, + ResourcesUsed, +} from "./otel/Views.ts"; + +/** + * Provisions an Axiom OTEL ingestion pipeline: + * + * - Three datasets (`{stage}-traces`, `{stage}-logs`, `{stage}-metrics`), + * each with the matching `otel:*:v1` `kind`. + * - One ingest-only API token scoped to those three datasets. + * - Outputs the OTLP endpoints + `Authorization` header value so callers can + * wire them straight into a Worker / Lambda's env vars. + * - Optionally syncs the same values to the GitHub repo's Actions secrets + * (set `SYNC_GITHUB_SECRETS=1` when deploying). + */ +export default Alchemy.Stack( + "AlchemyOtel", + { + providers: Layer.mergeAll( + Axiom.providers(), + Cloudflare.providers(), + GitHub.providers(), + ), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const traces = yield* Traces; + const logs = yield* Logs; + const metrics = yield* Metrics; + + // Public ingest relay. Bound to `otel.alchemy.run` and + // `analytics.alchemy.run` only in prod so dev stages exercise the same + // code path under a `*.workers.dev` URL. The same Worker also reverse- + // proxies PostHog Cloud for first-party browser analytics. + const relay = yield* Ingester; + + // const posthogProjectKey = yield* Config.string("POSTHOG_PROJECT_KEY") + // .pipe(Config.option) + // + // .pipe(Effect.orDie); + + // Saved APL queries — one per insight surfaced by the dashboard. + yield* ActiveUsersHourly; + yield* ActiveUsersByVersion; + yield* ActiveUsersByCi; + yield* ResourcesUsed; + yield* DeployDestroyLatency; + yield* ResourceLatency; + yield* CliInvocations; + yield* ResourceErrorRate; + + // Composed dashboard combining the same signals on a 12-col grid. + yield* CliOverviewDashboard; + + const env = { + OTEL_EXPORTER_OTLP_ENDPOINT: traces.otelEndpoint, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: traces.otelTracesEndpoint, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: logs.otelLogsEndpoint, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: metrics.otelMetricsEndpoint, + AXIOM_DATASET_TRACES: traces.name, + AXIOM_DATASET_LOGS: logs.name, + AXIOM_DATASET_METRICS: metrics.name, + RELAY_URL: relay.url, + }; + + if (process.env.SYNC_GITHUB_SECRETS === "1") { + yield* GitHub.Secrets({ + owner: "alchemy-run", + repository: "alchemy-effect", + secrets: { + AXIOM_DATASET_TRACES: traces.name, + AXIOM_DATASET_LOGS: logs.name, + AXIOM_DATASET_METRICS: metrics.name, + OTEL_EXPORTER_OTLP_ENDPOINT: traces.otelEndpoint, + }, + }); + } + + return env; + }), +); diff --git a/.repos/alchemy-effect/stacks/otel/Dashboard.ts b/.repos/alchemy-effect/stacks/otel/Dashboard.ts new file mode 100644 index 00000000000..dbe48247e5e --- /dev/null +++ b/.repos/alchemy-effect/stacks/otel/Dashboard.ts @@ -0,0 +1,688 @@ +import * as Alchemy from "alchemy"; +import * as Axiom from "alchemy/Axiom"; +import type { Input } from "alchemy/Input"; +import * as Output from "alchemy/Output"; +import { Effect } from "effect"; +import { Traces } from "./Datasets.ts"; + +/** + * `${stage} alchemy CLI overview` — a single dashboard answering the + * four questions we actually care about, laid out on a 12-column grid: + * + * 1. **How many active users are working on a project?** + * Distinct `alchemy.user.id` per `alchemy.git.origin_hash`. + * 2. **How many distinct projects are there?** + * Distinct `alchemy.git.origin_hash`. + * 3. **How many projects use CI/CD?** + * Distinct `alchemy.git.origin_hash` where `alchemy.ci=true`. + * 4. **What state stores are people using?** + * Sourced from `state_store.init` spans tagged with + * `alchemy.state_store.id` (the open-ended `StateService.id` + * slug; built-ins are `local` / `inmemory` / `http` / + * `cloudflare-http`, third-party stores get tracked automatically + * by setting their own slug). Emitted once per process via + * `recordStateStoreInit` at every `Layer.effect(State, …)` site. + * + * Project identity uses `alchemy.git.origin_hash` rather than + * `alchemy.user.id`: ephemeral CI runners regenerate `~/.alchemy/id` + * every job, which dramatically inflates the user count. The git + * origin hash is stable across runs of the same repo and is the + * closest proxy we have for "a project". + * + * All queries target `${stage}-traces` — Axiom's metrics datasets + * cannot be queried via APL, but every metric we emit has an + * equivalent span (`cli.`, `provider.`, + * `state_store.deploy`, `state_store.init`). + * + * Each chart's APL query is built with `Output.interpolate` against + * `traces.name` so Alchemy sequences the dashboard after the dataset + * exists. Otherwise Axiom would reject creation with + * `BadRequest: failed to validate ... entity not found`. + */ +export const CliOverviewDashboard = Axiom.Dashboard( + "CliOverview", + Effect.gen(function* () { + const stack = yield* Alchemy.Stack; + const traces = yield* Traces; + const t = traces.name; + const isProd = stack.stage === "prod"; + + const prodTracesName: Input = isProd + ? t + : (yield* Axiom.Dataset.ref("Traces", { stage: "prod" })).name; + // Global filter bar — a `SmartFilter` chart drives every other + // chart's APL via `declare query_parameters` parameters: + // + // - `alchemy_version` (every stage) narrows every chart to a + // single alchemy version. An explicit "All" row with an empty + // value short-circuits the filter via + // `isempty(alchemy_version)`. APL-sourced filters don't get + // the auto-"All" that `list` filters do, so the source query + // unions one in. + // + // - `dataset_filter` (non-prod stages only) lets us preview + // dashboard changes against `prod-traces` while defaulting to + // the current stage's own dataset. Charts reference the + // dataset via `table(dataset_filter)` instead of a literal + // `['']` so the parameter binding takes effect. On prod + // we skip the filter entirely and hardcode `['prod-traces']`. + // + // Fresh stages have no spans yet, so `service.version` may not + // exist as a column. `column_ifexists` lets the queries parse + // against an empty dataset (otherwise APL rejects them with + // `invalid field: "service.version"`). + // `bin_size` drives every TimeSeries chart's bucket width so a + // single filter flips the whole dashboard between hourly / daily / + // weekly aggregation. Queries reference it as + // `bin(_time, totimespan(bin_size))`. + const declarations: Input = isProd + ? `declare query_parameters (alchemy_version:string = "", bin_size:string = "1h");` + : Output.interpolate`declare query_parameters (dataset_filter:string = "${t}", alchemy_version:string = "", bin_size:string = "1h");`; + const datasetExpr: Input = isProd + ? Output.interpolate`['${t}']` + : "table(dataset_filter)"; + const versionFilterWhere = `\n | where isempty(alchemy_version) or tostring(column_ifexists("service.version", "")) == alchemy_version`; + const charts: Input[] = [ + { + id: "filter-bar", + type: "SmartFilter", + name: "Filters", + filters: [ + ...(isProd + ? [] + : [ + { + id: "dataset_filter", + type: "select" as const, + name: "dataset", + active: true, + selectType: "list" as const, + options: [ + { + id: "stage", + key: `${stack.stage} (this stage)`, + value: t, + default: true, + }, + { + id: "prod", + key: "prod (production data)", + value: prodTracesName, + }, + ], + }, + ]), + { + id: "bin_size", + type: "select", + name: "aggregation period", + active: true, + selectType: "list", + options: [ + { id: "1h", key: "1 hour", value: "1h", default: true }, + { id: "6h", key: "6 hours", value: "6h" }, + { id: "1d", key: "1 day", value: "1d" }, + { id: "7d", key: "7 days", value: "7d" }, + ], + }, + { + id: "alchemy_version", + type: "select", + name: "alchemy version", + active: true, + selectType: "apl", + options: [], + apl: { + apl: Output.interpolate` + ${declarations} + let opts = ${datasetExpr} + | extend sv = tostring(column_ifexists("service.version", "")) + | where sv != "" + | distinct sv + | sort by sv desc + | project key=sv, value=sv; + let all = print key="All", value=""; + union all, opts + `, + }, + }, + ], + }, + // Row 1 — top-line counts answering Qs 1, 2, 3. + { + id: "distinct-projects", + name: "Distinct projects (7d)", + type: "Statistic", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']) + | where project != "" + | summarize projects=dcount(project) + `, + }, + }, + { + id: "projects-using-ci", + name: "Projects using CI/CD (7d)", + type: "Statistic", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), + ci=tostring(['resource.custom']['alchemy.ci']) + | where project != "" and ci == "true" + | summarize projects=dcount(project) + `, + }, + }, + { + id: "active-users-7d", + name: "Active users — non-CI (7d)", + type: "Statistic", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend uid=tostring(['resource.custom']['alchemy.user.id']), + ci=tostring(['resource.custom']['alchemy.ci']) + | where ci != "true" + | summarize users=dcount(uid) + `, + }, + }, + + // Row 2 — Q1 broken out per-project, plus solo-vs-team split. + { + id: "users-per-project", + name: "Active users per project (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), + uid=tostring(['resource.custom']['alchemy.user.id']), + ci=tostring(['resource.custom']['alchemy.ci']) + | where project != "" + | summarize users_total=dcount(uid), + users_local=dcountif(uid, ci != "true"), + users_ci=dcountif(uid, ci == "true"), + events=count() + by project + | order by users_total desc + | take 100 + `, + }, + }, + { + id: "project-team-size-distribution", + name: "Project team-size distribution (local users / project)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), + uid=tostring(['resource.custom']['alchemy.user.id']), + ci=tostring(['resource.custom']['alchemy.ci']) + | where project != "" and ci != "true" + | summarize users=dcount(uid) by project + | extend bucket = case( + users == 1, "1 (solo)", + users <= 3, "2-3", + users <= 10, "4-10", + "11+") + | summarize projects=count() by bucket + | order by bucket asc + `, + }, + }, + + // Row 3 — Q4: state-store breakdown. + { + id: "state-store-projects-by-id", + name: "Projects by state store (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name == "state_store.init" + | extend store=tostring(['attributes.custom']['alchemy.state_store.id']), + project=tostring(['resource.custom']['alchemy.git.origin_hash']), + uid=tostring(['resource.custom']['alchemy.user.id']) + | summarize projects=dcountif(project, project != ""), + users=dcount(uid), + inits=count() + by store + | order by projects desc + `, + }, + }, + { + id: "state-store-by-id-over-time", + name: "State store init by store / hour", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name == "state_store.init" + | extend store=tostring(['attributes.custom']['alchemy.state_store.id']) + | summarize count=count() by store, bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + + // Row 4 — adoption shape: project growth + CI-vs-local split. + { + id: "projects-over-time", + name: "Distinct projects per day", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']) + | where project != "" + | summarize projects=dcount(project) by bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + { + id: "active-users-over-time-hourly", + name: "Active users (non-CI)", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend uid=tostring(['resource.custom']['alchemy.user.id']), + ci=tostring(['resource.custom']['alchemy.ci']) + | where ci != "true" and uid != "" + | summarize users=dcount(uid) by bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + { + id: "active-users-over-time-daily", + name: "Active users (CI vs local)", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend uid=tostring(['resource.custom']['alchemy.user.id']), + ci=tostring(['resource.custom']['alchemy.ci']) + | where uid != "" + | extend bucket=iff(ci == "true", "ci", "local") + | summarize users=dcount(uid) by bucket, bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + { + id: "ci-vs-local-projects", + name: "Projects: CI vs local per day", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), + ci=tostring(['resource.custom']['alchemy.ci']) + | where project != "" + | extend bucket=iff(ci == "true", "ci", "local") + | summarize projects=dcount(project) by bucket, bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + + // Row 5 — keep the state-store deploy health signals for the + // Cloudflare-hosted store (not just init, but actual deploys). + { + id: "state-store-deploys", + name: "Cloudflare state store deploys (success vs error)", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name == "state_store.deploy" + | extend status=iff(tobool(['error']), "error", "success") + | summarize count=count() by status, bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + { + id: "active-users-by-version", + name: "Active users by alchemy version (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | extend uid=tostring(['resource.custom']['alchemy.user.id']), + version=tostring(['resource.custom']['alchemy.version']), + ci=tostring(['resource.custom']['alchemy.ci']) + | summarize users_total=dcount(uid), + users_local=dcountif(uid, ci != "true"), + users_ci=dcountif(uid, ci == "true") + by version + | order by users_total desc + `, + }, + }, + + // ─── Cloudflare State Store ──────────────────────────────── + // + // Two span sources feed this section: + // + // - **CLI spans** (`state_store.init`, `state_store.bootstrap`, + // `state_store.deploy`, ...) — emitted from the user's + // terminal/CI. Carry `alchemy.cloudflare.account_hash` (a + // SHA-256 of the CF accountId) so we can count distinct + // deployments without leaking raw account IDs. + // + // - **Worker handler spans** (`state_store.{getVersion, + // listStacks,listStages,listResources,getState,setState, + // deleteState,getReplacedResources,deleteStack}`) — emitted + // from inside the deployed `alchemy-state-store` worker. + // Distinguished by the `alchemy.state_store.script_name` + // **resource** attribute (set on the worker's OTLP tracer + // resource); CLI spans do not carry that resource attr. + // + // Worker handler spans carry `alchemy.state_store.{op,stack, + // stage,fqn}` as span attributes plus a `duration` field — + // that's what powers the latency / op-rate / stack-count + // charts. Account-hash sits on CLI spans only (worker-side + // injection would need a separate deploy-time env-var pass). + { + id: "state-store-distinct-cloudflare-stores", + name: "Distinct Cloudflare state-store deployments (7d)", + type: "Statistic", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name == "state_store.init" + | extend hash=tostring(['attributes.custom']['alchemy.cloudflare.account_hash']) + | where hash != "" + | summarize stores=dcount(hash) + `, + }, + }, + { + id: "state-store-distinct-stacks", + name: "Distinct stacks tracked (7d)", + type: "Statistic", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where ['resource.custom']['alchemy.state_store.script_name'] != "" + | extend stack=tostring(['attributes.custom']['alchemy.state_store.stack']) + | where stack != "" + | summarize stacks=dcount(stack) + `, + }, + }, + { + id: "state-store-total-ops", + name: "Cloudflare state store ops (7d)", + type: "Statistic", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where ['resource.custom']['alchemy.state_store.script_name'] != "" + | extend op=tostring(['attributes.custom']['alchemy.state_store.op']) + | where op != "" + | summarize ops=count() + `, + }, + }, + { + id: "state-store-op-latency", + name: "State store op latency (p50 / p95 / p99, 7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where ['resource.custom']['alchemy.state_store.script_name'] != "" + | extend op=tostring(['attributes.custom']['alchemy.state_store.op']) + | where op != "" + | summarize p50=percentile(duration, 50), + p95=percentile(duration, 95), + p99=percentile(duration, 99), + calls=count() + by op + | order by calls desc + `, + }, + }, + { + id: "state-store-ops-over-time", + name: "State store ops over time (by op)", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where ['resource.custom']['alchemy.state_store.script_name'] != "" + | extend op=tostring(['attributes.custom']['alchemy.state_store.op']) + | where op != "" + | summarize count=count() by op, bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + { + id: "state-store-error-rate", + name: "State store error rate by op (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where ['resource.custom']['alchemy.state_store.script_name'] != "" + | extend op=tostring(['attributes.custom']['alchemy.state_store.op']), + err=tobool(['error']) + | where op != "" + | summarize errors=countif(err == true), + total=count() + by op + | extend error_rate=todouble(errors) / todouble(total) + | order by total desc + `, + }, + }, + { + id: "state-store-top-stacks", + name: "Top stacks by activity (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where ['resource.custom']['alchemy.state_store.script_name'] != "" + | extend stack=tostring(['attributes.custom']['alchemy.state_store.stack']) + | where stack != "" + | summarize ops=count() by stack + | order by ops desc + | take 25 + `, + }, + }, + { + id: "state-store-deployments-over-time", + name: "Distinct Cloudflare state-store deployments per day", + type: "TimeSeries", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name == "state_store.init" + | extend hash=tostring(['attributes.custom']['alchemy.cloudflare.account_hash']) + | where hash != "" + | summarize stores=dcount(hash) by bin(_time, totimespan(bin_size)) + | order by _time asc + `, + }, + }, + + // ─── Resource usage & reliability ────────────────────────── + // + // Every provider lifecycle invocation is wrapped in a + // `provider.` span (see `instrumentLifecycle` in Apply.ts) + // carrying `alchemy.resource.type` and `alchemy.resource.op` + // (precreate/create/update/delete/read). `error` is set when + // the lifecycle effect fails. + { + id: "resource-usage-ranked", + name: "Resource usage — ranked by lifecycle ops (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name startswith "provider." + | extend resource_type=tostring(['attributes.custom']['alchemy.resource.type']), + project=tostring(['resource.custom']['alchemy.git.origin_hash']) + | where resource_type != "" + | summarize ops=count(), + projects=dcountif(project, project != ""), + errors=countif(tobool(['error']) == true) + by resource_type + | order by ops desc + | take 200 + `, + }, + }, + { + id: "resource-error-rate-by-type", + name: "Resource error rate by resource type (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name startswith "provider." + | extend resource_type=tostring(['attributes.custom']['alchemy.resource.type']), + err=tobool(['error']) + | where resource_type != "" + | summarize errors=countif(err == true), + total=count() + by resource_type + | extend error_rate=todouble(errors) / todouble(total) + | order by error_rate desc, total desc + | take 200 + `, + }, + }, + { + id: "resource-error-rate-by-op", + name: "Resource error rate by lifecycle method (7d)", + type: "Table", + query: { + apl: Output.interpolate` + ${declarations} + ${datasetExpr}${versionFilterWhere} + | where name startswith "provider." + | extend op=tostring(['attributes.custom']['alchemy.resource.op']), + err=tobool(['error']) + | where op != "" + | summarize errors=countif(err == true), + total=count() + by op + | extend error_rate=todouble(errors) / todouble(total) + | order by total desc + `, + }, + }, + ]; + + const layout: Axiom.LayoutCell[] = [ + // Row 0 — filter bar (full width, narrow) + { i: "filter-bar", x: 0, y: 0, w: 12, h: 2 }, + // Row 1 + { i: "distinct-projects", x: 0, y: 2, w: 4, h: 4 }, + { i: "projects-using-ci", x: 4, y: 2, w: 4, h: 4 }, + { i: "active-users-7d", x: 8, y: 2, w: 4, h: 4 }, + // Row 2 — active users over time + { i: "active-users-over-time-hourly", x: 0, y: 6, w: 6, h: 6 }, + { i: "active-users-over-time-daily", x: 6, y: 6, w: 6, h: 6 }, + // Row 3 + { i: "users-per-project", x: 0, y: 12, w: 8, h: 8 }, + { i: "project-team-size-distribution", x: 8, y: 12, w: 4, h: 8 }, + // Row 4 + { i: "state-store-projects-by-id", x: 0, y: 20, w: 6, h: 6 }, + { i: "state-store-by-id-over-time", x: 6, y: 20, w: 6, h: 6 }, + // Row 5 + { i: "projects-over-time", x: 0, y: 26, w: 6, h: 6 }, + { i: "ci-vs-local-projects", x: 6, y: 26, w: 6, h: 6 }, + // Row 6 + { i: "state-store-deploys", x: 0, y: 32, w: 6, h: 6 }, + { i: "active-users-by-version", x: 6, y: 32, w: 6, h: 6 }, + // Row 7 — Cloudflare State Store: top-line stats + { + i: "state-store-distinct-cloudflare-stores", + x: 0, + y: 38, + w: 4, + h: 4, + }, + { i: "state-store-distinct-stacks", x: 4, y: 38, w: 4, h: 4 }, + { i: "state-store-total-ops", x: 8, y: 38, w: 4, h: 4 }, + // Row 8 — performance: latency table (full width) + { i: "state-store-op-latency", x: 0, y: 42, w: 12, h: 8 }, + // Row 9 — ops over time + error rate + { i: "state-store-ops-over-time", x: 0, y: 50, w: 8, h: 6 }, + { i: "state-store-error-rate", x: 8, y: 50, w: 4, h: 6 }, + // Row 10 — distribution + adoption shape + { i: "state-store-top-stacks", x: 0, y: 56, w: 6, h: 8 }, + { i: "state-store-deployments-over-time", x: 6, y: 56, w: 6, h: 8 }, + // Row 11 — Resource usage ranking (full width) + { i: "resource-usage-ranked", x: 0, y: 64, w: 12, h: 10 }, + // Row 12 — Error rate broken down by resource type and lifecycle method + { i: "resource-error-rate-by-type", x: 0, y: 74, w: 8, h: 10 }, + { i: "resource-error-rate-by-op", x: 8, y: 74, w: 4, h: 10 }, + ]; + + return { + dashboard: { + name: `${stack.stage} alchemy CLI overview`, + // Empty owner = X-AXIOM-EVERYONE (org-shared). API tokens + // can't create per-user "private" dashboards. + owner: "", + description: + "Adoption telemetry: distinct projects, active users per project, " + + "CI vs local usage, and state-store backend breakdown. Plus " + + "Cloudflare State Store ops: distinct deployments, stack count, " + + "per-op latency / error rate. " + + "Resource usage: ranked list of resource types by lifecycle ops, " + + "with error rates broken down by resource type and by lifecycle method. " + + "Project identity = alchemy.git.origin_hash (stable across CI runs); " + + "user identity = alchemy.user.id (ephemeral in CI); " + + "Cloudflare deployment identity = alchemy.cloudflare.account_hash " + + "(SHA-256 of accountId, set on CLI state_store.* spans).", + refreshTime: 60 as const, + schemaVersion: 2 as const, + // Axiom requires the `qr-now-{duration}` form for relative times. + timeWindowStart: "qr-now-7d", + timeWindowEnd: "qr-now", + charts, + layout, + }, + }; + }), +); diff --git a/.repos/alchemy-effect/stacks/otel/Datasets.ts b/.repos/alchemy-effect/stacks/otel/Datasets.ts new file mode 100644 index 00000000000..76d1614f7f5 --- /dev/null +++ b/.repos/alchemy-effect/stacks/otel/Datasets.ts @@ -0,0 +1,35 @@ +import * as Axiom from "alchemy/Axiom"; +import { Stack } from "alchemy/Stack"; + +export const Traces = Axiom.Dataset( + "Traces", + Stack.useSync(({ stage }) => ({ + name: `${stage}-traces`, + kind: "otel:traces:v1" as const, + description: `OTEL traces for stage '${stage}'`, + retentionDays: 30, + useRetentionPeriod: true, + })), +); + +export const Logs = Axiom.Dataset( + "Logs", + Stack.useSync(({ stage }) => ({ + name: `${stage}-logs`, + kind: "otel:logs:v1" as const, + description: `OTEL logs for stage '${stage}'`, + retentionDays: 30, + useRetentionPeriod: true, + })), +); + +export const Metrics = Axiom.Dataset( + "Metrics", + Stack.useSync(({ stage }) => ({ + name: `${stage}-metrics`, + kind: "otel:metrics:v1" as const, + description: `OTEL metrics for stage '${stage}'`, + retentionDays: 30, + useRetentionPeriod: true, + })), +); diff --git a/.repos/alchemy-effect/stacks/otel/IngestToken.ts b/.repos/alchemy-effect/stacks/otel/IngestToken.ts new file mode 100644 index 00000000000..07d143a33b3 --- /dev/null +++ b/.repos/alchemy-effect/stacks/otel/IngestToken.ts @@ -0,0 +1,28 @@ +import * as Alchemy from "alchemy"; +import * as Axiom from "alchemy/Axiom"; +import * as Output from "alchemy/Output"; +import { Effect } from "effect"; +import { Logs, Metrics, Traces } from "./Datasets.ts"; + +export const IngestToken = Axiom.ApiToken( + "IngestToken", + Effect.all([Alchemy.Stack, Traces, Logs, Metrics]).pipe( + Effect.map(([stack, traces, logs, metrics]) => ({ + name: `${stack.stage}-otel-ingest`, + description: `Ingest-only token for ${stack.stage} OTEL datasets`, + // Reference dataset Outputs (rather than literal strings) so Alchemy + // sequences the token after the datasets exist. + datasetCapabilities: Output.all( + traces.name, + logs.name, + metrics.name, + ).pipe( + Output.map(([t, l, m]) => ({ + [t]: { ingest: ["create"] as const }, + [l]: { ingest: ["create"] as const }, + [m]: { ingest: ["create"] as const }, + })), + ), + })), + ), +); diff --git a/.repos/alchemy-effect/stacks/otel/Ingester.ts b/.repos/alchemy-effect/stacks/otel/Ingester.ts new file mode 100644 index 00000000000..8a1f6b6150c --- /dev/null +++ b/.repos/alchemy-effect/stacks/otel/Ingester.ts @@ -0,0 +1,159 @@ +import * as Cloudflare from "alchemy/Cloudflare"; +import { Stack } from "alchemy/Stack"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { Logs, Metrics, Traces } from "./Datasets.ts"; +import { IngestToken } from "./IngestToken.ts"; + +/** + * Public ingest relay used by both: + * + * 1. The alchemy CLI / services posting OTLP/JSON to `/v1/{traces,logs,metrics}`, + * which we forward to Axiom with the ingest bearer token attached server-side. + * 2. The website's PostHog browser SDK, which posts to anything else + * (`/e`, `/s`, `/flags`, `/decide`, `/static/*`, ...). PostHog ingest needs + * no server secret, so we just reverse-proxy to PostHog Cloud (US region). + * + * Bound to two custom domains in prod: + * - `otel.alchemy.run` — primary OTLP entrypoint + * - `analytics.alchemy.run` — first-party PostHog ingest (defeats ad-blockers) + * + * Environment (set by `stacks/otel.ts`): + * - `AXIOM_TRACES_ENDPOINT` — full Axiom OTLP traces URL + * - `AXIOM_LOGS_ENDPOINT` — full Axiom OTLP logs URL + * - `AXIOM_METRICS_ENDPOINT` — full Axiom OTLP metrics URL + * - `AXIOM_INGEST_TOKEN` — Bearer token (Redacted) + */ +export default class Ingester extends Cloudflare.Worker()( + "OtelWorker", + Stack.useSync(({ stage }) => ({ + main: import.meta.filename, + observability: { enabled: true }, + domain: + stage === "prod" + ? ["otel.alchemy.run", "analytics.alchemy.run"] + : undefined, + compatibility: { + date: "2026-03-17", + flags: ["nodejs_compat"], + }, + })), + Effect.gen(function* () { + const tokenValue = yield* (yield* IngestToken).token; + const traces = yield* Traces; + const logs = yield* Logs; + const metrics = yield* Metrics; + const tracesEndpoint = yield* traces.otelTracesEndpoint; + const logsEndpoint = yield* logs.otelLogsEndpoint; + const metricsEndpoint = yield* metrics.otelMetricsEndpoint; + const tracesDataset = yield* traces.name; + const logsDataset = yield* logs.name; + const metricsDataset = yield* metrics.name; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + const url = new URL(request.url, "http://x"); + const path = url.pathname; + + // 1. OTLP → Axiom (POST /v1/{traces,logs,metrics}) + const otlp = + request.method === "POST" + ? path === "/v1/traces" + ? { + endpoint: yield* tracesEndpoint, + dataset: yield* tracesDataset, + } + : path === "/v1/logs" + ? { endpoint: yield* logsEndpoint, dataset: yield* logsDataset } + : path === "/v1/metrics" + ? { + endpoint: yield* metricsEndpoint, + dataset: yield* metricsDataset, + } + : undefined + : undefined; + + if (otlp) { + const tokenRaw = yield* tokenValue.pipe(Effect.map(Redacted.value)); + const token = Redacted.isRedacted(tokenRaw) + ? Redacted.value(tokenRaw) + : (tokenRaw as string); + + const body = yield* request.arrayBuffer; + + const response = yield* Effect.tryPromise({ + try: () => + fetch(otlp.endpoint, { + method: "POST", + headers: { + "content-type": + request.headers["content-type"] ?? "application/json", + authorization: `Bearer ${token}`, + "x-axiom-dataset": otlp.dataset, + }, + body, + }), + catch: (e) => (e instanceof Error ? e : new Error(String(e))), + }); + + return HttpServerResponse.fromWeb(response); + } + + // 2. Everything else → PostHog Cloud (US region) + // /static/* and /array/* live on the assets host; everything else on the + // ingest host. Static assets are cacheable at the edge. + const isAsset = + path.startsWith("/static/") || path.startsWith("/array/"); + const upstreamHost = isAsset + ? "https://us-assets.i.posthog.com" + : "https://us.i.posthog.com"; + const target = `${upstreamHost}${path}${url.search}`; + + const raw = yield* HttpServerRequest.toWeb(request); + + // Drop hop-by-hop / Cloudflare-internal headers; preserve everything + // else (content-type, accept, user-agent, x-forwarded-for, etc.). + const headers = new Headers(raw.headers); + headers.delete("host"); + headers.delete("cookie"); + for (const key of [...headers.keys()]) { + if (key.startsWith("cf-")) headers.delete(key); + } + const cfIp = + raw.headers.get("cf-connecting-ip") ?? + raw.headers.get("x-real-ip") ?? + undefined; + if (cfIp) headers.set("x-forwarded-for", cfIp); + + const upstream = yield* Effect.tryPromise({ + try: () => + fetch(target, { + method: raw.method, + headers, + body: + raw.method === "GET" || raw.method === "HEAD" ? null : raw.body, + redirect: "manual", + ...(isAsset + ? // Cache the SDK loader / array bundles at the edge for an hour. + ({ cf: { cacheTtl: 3600, cacheEverything: true } } as any) + : {}), + }), + catch: (e) => (e instanceof Error ? e : new Error(String(e))), + }); + + return HttpServerResponse.fromWeb(upstream); + }).pipe( + Effect.catch((err) => + Effect.succeed( + HttpServerResponse.text(`Relay error: ${err.message}`, { + status: 502, + }), + ), + ), + ), + }; + }), +) {} diff --git a/.repos/alchemy-effect/stacks/otel/Views.ts b/.repos/alchemy-effect/stacks/otel/Views.ts new file mode 100644 index 00000000000..f91d0571387 --- /dev/null +++ b/.repos/alchemy-effect/stacks/otel/Views.ts @@ -0,0 +1,186 @@ +import * as Alchemy from "alchemy"; +import * as Axiom from "alchemy/Axiom"; +import * as Output from "alchemy/Output"; +import { Effect } from "effect"; +import { Traces } from "./Datasets.ts"; + +/** + * Saved Axiom views (APL queries) for the alchemy CLI's OTEL signals. + * + * All views target `${stage}-traces`. Axiom's metrics datasets cannot be + * queried via APL ("Please use the builder to query metrics datasets"), + * but every metric we emit has an equivalent span — `cli.` for + * invocations, `provider.` for resource lifecycle ops with + * `duration` for latency and `error` for status — so we derive + * everything from traces. + * + * Each view's `datasets` entry references `traces.name` (an Output) rather + * than a literal `${stage}-traces` string so Alchemy sequences view creation + * after the dataset exists. Without this, Axiom rejects the view with + * `BadRequest: failed to validate view: entity not found`. + * + * Attribute paths: + * - Resource attributes (per-process, set on `OtlpTracer.resource`): + * `['resource.custom']['alchemy.user.id']`, + * `['resource.custom']['alchemy.version']`, … + * - Span attributes (per-span, set on `Effect.withSpan(... { attributes })`): + * `['attributes.custom']['alchemy.resource.type']`, + * `['attributes.custom']['alchemy.resource.op']`, … + * + * `error` (bool) and `status.code` are first-class span fields. + */ + +const viewProps = ( + fn: (ctx: { stage: string; traces: Output.Output }) => A, +) => + Effect.all([Alchemy.Stack, Traces]).pipe( + Effect.map(([stack, traces]) => + fn({ stage: stack.stage, traces: traces.name }), + ), + ); + +export const ActiveUsersHourly = Axiom.View( + "ActiveUsersHourly", + viewProps(({ stage, traces }) => ({ + name: `${stage}-active-users-hourly`, + description: "Distinct alchemy.user.id per hour, last 7d", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | extend uid=tostring(['resource.custom']['alchemy.user.id']) + | summarize users=dcount(uid) by bin(_time, 1h) + | order by _time asc + `, + })), +); + +export const ActiveUsersByVersion = Axiom.View( + "ActiveUsersByVersion", + viewProps(({ stage, traces }) => ({ + name: `${stage}-active-users-by-version`, + description: "Distinct users grouped by alchemy.version (last 7d)", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | extend uid=tostring(['resource.custom']['alchemy.user.id']), + version=tostring(['resource.custom']['alchemy.version']) + | summarize users=dcount(uid) by version + | order by users desc + `, + })), +); + +export const ActiveUsersByCi = Axiom.View( + "ActiveUsersByCi", + viewProps(({ stage, traces }) => ({ + name: `${stage}-active-users-by-ci`, + description: "CI vs local users (last 7d)", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | extend uid=tostring(['resource.custom']['alchemy.user.id']), + ci=tostring(['resource.custom']['alchemy.ci']) + | summarize users=dcount(uid) by ci + `, + })), +); + +export const ResourcesUsed = Axiom.View( + "ResourcesUsed", + viewProps(({ stage, traces }) => ({ + name: `${stage}-resources-used`, + description: "Top resource types by lifecycle op count (last 7d)", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | where name startswith "provider." + | extend rt=tostring(['attributes.custom']['alchemy.resource.type']), + op=tostring(['attributes.custom']['alchemy.resource.op']) + | summarize ops=count() by rt, op + | order by ops desc + `, + })), +); + +export const DeployDestroyLatency = Axiom.View( + "DeployDestroyLatency", + viewProps(({ stage, traces }) => ({ + name: `${stage}-deploy-destroy-latency`, + description: "p50/p95/p99 of cli.deploy and cli.destroy spans", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | where name in ("cli.deploy", "cli.destroy") + | summarize p50=percentile(duration, 50), + p95=percentile(duration, 95), + p99=percentile(duration, 99) + by name, bin(_time, 1h) + | order by _time asc + `, + })), +); + +export const ResourceLatency = Axiom.View( + "ResourceLatency", + viewProps(({ stage, traces }) => ({ + name: `${stage}-resource-latency`, + description: + "p50/p95 of provider. spans by resource_type and op (last 7d)", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | where name startswith "provider." + | extend rt=tostring(['attributes.custom']['alchemy.resource.type']), + op=tostring(['attributes.custom']['alchemy.resource.op']) + | summarize p50=percentile(duration, 50), + p95=percentile(duration, 95), + count=count() + by rt, op + | order by p95 desc + `, + })), +); + +export const CliInvocations = Axiom.View( + "CliInvocations", + viewProps(({ stage, traces }) => ({ + name: `${stage}-cli-invocations`, + description: + "cli. span counts grouped by command and success/error", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | where name startswith "cli." + | extend command=extract("cli\\\\.(.+)", 1, name), + status=iff(tobool(['error']), "error", "success") + | summarize count=count() by command, status, bin(_time, 1h) + | order by _time asc + `, + })), +); + +export const ResourceErrorRate = Axiom.View( + "ResourceErrorRate", + viewProps(({ stage, traces }) => ({ + name: `${stage}-resource-error-rate`, + description: + "provider. spans split by status (success vs error) per hour", + datasets: [traces], + aplQuery: ` + ['${stage}-traces'] + | where _time > ago(7d) + | where name startswith "provider." + | extend rt=tostring(['attributes.custom']['alchemy.resource.type']), + status=iff(tobool(['error']), "error", "success") + | summarize total=count() by rt, status, bin(_time, 1h) + | order by _time asc + `, + })), +); diff --git a/.repos/alchemy-effect/stacks/pr-package.ts b/.repos/alchemy-effect/stacks/pr-package.ts new file mode 100644 index 00000000000..16bb9c463e1 --- /dev/null +++ b/.repos/alchemy-effect/stacks/pr-package.ts @@ -0,0 +1,21 @@ +import * as PrPackage from "@alchemy.run/pr-package"; +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as Effect from "effect/Effect"; +import Api from "./pr-package/Api.ts"; + +export default Alchemy.Stack( + "PrPackage", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const authToken = yield* PrPackage.AuthTokenValue; + const api = yield* Api; + return { + url: api.url.as(), + authToken: authToken.text, + }; + }), +); diff --git a/.repos/alchemy-effect/stacks/pr-package/Api.ts b/.repos/alchemy-effect/stacks/pr-package/Api.ts new file mode 100644 index 00000000000..ac32ff777ce --- /dev/null +++ b/.repos/alchemy-effect/stacks/pr-package/Api.ts @@ -0,0 +1,57 @@ +import * as PrPackage from "@alchemy.run/pr-package"; +import * as Cloudflare from "alchemy/Cloudflare"; +import { Stack } from "alchemy/Stack"; + +export const ALCHEMY_HOSTS = [ + "pkg.alchemy.run", + "xn--cu8h.alchemy.run", // 📦.alchemy.run +]; + +export const DISTILLED_HOSTS = [ + "pkg.distilled.cloud", + "xn--cu8h.distilled.cloud", // 📦.distilled.cloud +]; + +const parseAliasUrl: PrPackage.ParseAliasUrl = (url) => { + const segments = url.pathname + .split("/") + .filter(Boolean) + .map((s) => { + try { + return decodeURIComponent(s); + } catch { + return s; + } + }); + + if (url.hostname === "pkg.ing" || ALCHEMY_HOSTS.includes(url.hostname)) { + if (segments.length === 2) { + return { pkgName: segments[0]!, tag: segments[1]! }; + } + if (segments.length === 3 && segments[0]!.startsWith("@")) { + return { pkgName: `${segments[0]!}/${segments[1]!}`, tag: segments[2]! }; + } + } else if (DISTILLED_HOSTS.includes(url.hostname)) { + if (segments.length === 2) { + return { pkgName: `@distilled.cloud/${segments[0]!}`, tag: segments[1]! }; + } + } + return null; +}; + +export default class Api extends Cloudflare.Worker()( + "PrPackageWorker", + Stack.useSync(({ stage }) => ({ + main: import.meta.filename, + url: true, + domain: + stage === "prod" + ? ["pkg.ing", ...ALCHEMY_HOSTS, ...DISTILLED_HOSTS] + : undefined, + compatibility: { + flags: ["nodejs_compat"], + date: "2026-03-17", + }, + })), + PrPackage.handler({ parseAliasUrl }), +) {} diff --git a/.repos/alchemy-effect/test/smoke.test.ts b/.repos/alchemy-effect/test/smoke.test.ts new file mode 100644 index 00000000000..6837ee69e22 --- /dev/null +++ b/.repos/alchemy-effect/test/smoke.test.ts @@ -0,0 +1,685 @@ +/** + * Smoke test suite that exercises `alchemy destroy → deploy → destroy` in each + * example directory with both `bun` and `pnpm`. Commands run in-place against + * whatever is currently installed in the workspace; stdio is inherited so + * output streams directly to the terminal. + * + * Modes: + * default → test against the workspace `workspace:*` deps as-is + * SMOKE_CANARY=1 → pack + publish alchemy / better-auth / pr-package + * tarballs to pkg.ing under a fresh tag, add the + * pkg.ing URLs to the root workspace catalog, rewrite + * each example's `workspace:*` refs to `catalog:`, + * run a single root install, then `git checkout` the + * mutated package.json files and reinstall once on + * the way out. + * + * Env vars: + * SMOKE_RUNTIME `bun` or `pnpm` (default: "bun") + * SMOKE_CANARY "1" to enable canary mode (default: off) + * SMOKE_STAGE stage prefix, e.g. `pr-123` or `main` (default: "smoke") + * PKGING_HOST pkg.ing host (default: pkg.ing) + * + * Run with: `bun test ./test/smoke.test.ts`. + */ +import { $ } from "bun"; +import { afterAll, beforeAll, expect, test } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, ".."); +const TIMEOUT = 10 * 60 * 1000; + +const examples = [ + "aws-lambda", + "aws-lambda-httpapi", + "aws-lambda-rpc", + "cloudflare-git-artifacts", + "cloudflare-neon-drizzle", + "cloudflare-secrets-store", + "cloudflare-tanstack", + "cloudflare-vue", + // "cloudflare-solidstart", + // "cloudflare-solidjs-ssr", + "cloudflare-worker-async", + "cloudflare-worker", +]; +const ALL_RUNTIMES = ["bun", "pnpm"] as const; +type Runtime = (typeof ALL_RUNTIMES)[number]; + +// `SMOKE_RUNTIME` is the CI escape hatch — the matrix workflow runs one +// job per runtime so each can do its own ` install` and isolate +// from the other. Unset locally, the test runs both runtimes against +// each example with `bun` going first so it doesn't race `pnpm` on +// shared build outputs (vite `dist/`, `.alchemy/`). +const RUNTIMES: readonly Runtime[] = (() => { + const filter = process.env.SMOKE_RUNTIME?.trim(); + if (!filter) return ALL_RUNTIMES; + if (filter !== "bun" && filter !== "pnpm") { + throw new Error(`SMOKE_RUNTIME must be "bun" or "pnpm" (got: ${filter})`); + } + return [filter]; +})(); + +const PUBLISHED = [ + { dir: "alchemy", name: "alchemy" }, + { dir: "better-auth", name: "@alchemy.run/better-auth" }, + { dir: "pr-package", name: "@alchemy.run/pr-package" }, +] as const; + +const canary = process.env.SMOKE_CANARY === "1"; +const host = process.env.PKGING_HOST ?? "pkg.ing"; + +async function run( + cmd: string[] | readonly string[], + cwd: string, + env?: Record, +): Promise { + const proc = Bun.spawn([...cmd], { + cwd, + stdout: "inherit", + stderr: "inherit", + env: { ...process.env, ALCHEMY_NO_TUI: "1", ...env }, + }); + return await proc.exited; +} + +// Install every active runtime so each runtime's exec works against a +// node_modules layout it understands. Bun runs first because it writes +// the flat layout; pnpm 11 then layers its `.pnpm` virtual store on top +// so `pnpm exec` doesn't trip its implicit `runDepsStatusCheck` install +// inside an example directory at test time. When `SMOKE_RUNTIME` pins a +// single runtime, only that one installs. +// +// Canary mode mutates example package.json files at runtime, so the +// lockfile is intentionally stale during the run — `--no-frozen-lockfile` +// lets the install resolve the new `catalog:` refs. CI defaults pnpm to +// frozen-lockfile, which would otherwise fail with ERR_PNPM_OUTDATED_LOCKFILE. +// +// `Bun.spawn` has no TTY, so pnpm 11 aborts non-interactive `node_modules` +// removal with `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY` unless we set +// `CI=1`. Only forwarded to pnpm — alchemy's `loadFromEnv` short-circuits +// to env-only credentials when `CI=true`, which would break local runs +// that rely on the AWS SDK credential chain. +type InstallStep = { cmd: readonly string[]; env?: Record }; +const installCmds = (): readonly InstallStep[] => { + const cmds: InstallStep[] = []; + if (RUNTIMES.includes("bun")) { + cmds.push({ cmd: ["bun", "install", "--no-frozen-lockfile"] }); + } + if (RUNTIMES.includes("pnpm")) { + cmds.push({ + cmd: ["pnpm", "install", "--no-frozen-lockfile"], + env: { CI: "1" }, + }); + } + return cmds; +}; + +const installAll = async (): Promise => { + for (const { cmd, env } of installCmds()) { + expect(await run(cmd, ROOT, env)).toBe(0); + } +}; + +const ROOT_PKG_PATH = path.join(ROOT, "package.json"); +const examplePkgPath = (e: string) => + path.join(ROOT, "examples", e, "package.json"); + +type Pkg = { + workspaces?: { catalog?: Record }; + dependencies?: Record; + devDependencies?: Record; +}; + +const readJson = async (p: string): Promise => + JSON.parse(await fs.readFile(p, "utf8")) as T; +const writeJson = async (p: string, v: unknown) => + fs.writeFile(p, `${JSON.stringify(v, null, 2)}\n`); + +const PNPM_WORKSPACE_PATH = path.join(ROOT, "pnpm-workspace.yaml"); + +/** + * pnpm 11 runs an implicit `runDepsStatusCheck` before every `pnpm exec`, + * which performs a hidden `pnpm install`. Bun's catalog config lives in + * `package.json#workspaces.catalog` — pnpm doesn't read it. So we mirror + * the bun catalog (and workspaces list) into a `pnpm-workspace.yaml` for + * the duration of the suite. Generated, never checked in; cleaned up in + * `afterAll` and on SIGINT/SIGTERM. + * + * Delegates to `scripts/pnpm-workspace.ts` so the file shape (pinned + * catalog versions resolved from `bun.lock`, build-script allowlist) is + * identical to what CI's pre-`pnpm install` step writes — otherwise the + * smoke test would clobber CI's pinned catalogs with loose ranges and + * trip `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` on every per-example install. + */ +const writePnpmWorkspace = async () => { + const code = await run( + ["bun", path.join(ROOT, "scripts", "pnpm-workspace.ts")], + ROOT, + ); + if (code !== 0) { + throw new Error(`scripts/pnpm-workspace.ts exited with code ${code}`); + } +}; + +const removePnpmWorkspace = async () => { + const code = await run( + ["bun", path.join(ROOT, "scripts", "pnpm-workspace.ts"), "--remove"], + ROOT, + ); + if (code !== 0) { + // Best-effort — never fail teardown over a missing file. + await fs.rm(PNPM_WORKSPACE_PATH, { force: true }); + } +}; + +if (canary) { + beforeAll(async () => { + // CI passes PR_PACKAGE_TOKEN directly via env (matches pr-package.yaml); + // locally we fall back to `doppler` so contributors don't have to export + // the secret manually. Either path works. + let token = process.env.PR_PACKAGE_TOKEN?.trim() ?? ""; + if (!token) { + try { + token = ( + await $`doppler secrets get PR_PACKAGE_TOKEN --plain -p alchemy-v2 -c dev` + .quiet() + .text() + ).trim(); + } catch { + // doppler not installed / not authed — leave token empty, error below + } + } + if (!token) { + throw new Error( + "PR_PACKAGE_TOKEN is not set in env and `doppler -p alchemy-v2 -c dev` did not return one. " + + "Either export PR_PACKAGE_TOKEN, run `bun download:env`, or invoke via `doppler run`.", + ); + } + + const sha = (await $`git rev-parse HEAD`.quiet().text()).trim().slice(0, 7); + const stamp = new Date() + .toISOString() + .replace(/[-:T]/g, "") + .replace(/\..*/, ""); + const tag = `canary-${sha}-${stamp}`; + const tags = JSON.stringify([tag, "canary"]); + console.log(`→ canary tag: ${tag} (host=${host})`); + + expect(await run(["bun", "run", "build:packages"], ROOT)).toBe(0); + + for (const { dir, name } of PUBLISHED) { + const pkgDir = path.join(ROOT, "packages", dir); + for (const f of await fs.readdir(pkgDir)) { + if (f.endsWith(".tgz")) await fs.rm(path.join(pkgDir, f)); + } + expect( + await run(["bun", "pm", "pack", "--destination", "."], pkgDir), + ).toBe(0); + const tgz = (await fs.readdir(pkgDir)).find((f) => f.endsWith(".tgz")); + if (!tgz) throw new Error(`no tgz produced in ${pkgDir}`); + const abs = path.join(pkgDir, tgz); + console.log(`→ publish ${name} (${tgz})`); + const res = await fetch(`https://${host}/projects/${name}/packages`, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + "X-Tags": tags, + "X-TTL": "1 hour", + "Content-Type": "application/gzip", + }, + body: Bun.file(abs), + }); + if (!res.ok) { + throw new Error( + `publish ${name} failed: ${res.status} ${res.statusText}\n${await res.text()}`, + ); + } + } + + // Add the canary tarball URLs to the root catalog and rewrite each + // example's `workspace:*` ref for a published package to `catalog:`. + // One install at the root then resolves every example at once — much + // faster than ` add` per (example × published package). + const rootPkg = await readJson(ROOT_PKG_PATH); + rootPkg.workspaces ??= {}; + rootPkg.workspaces.catalog ??= {}; + for (const { name } of PUBLISHED) { + rootPkg.workspaces.catalog[name] = `https://${host}/${name}/${tag}`; + } + await writeJson(ROOT_PKG_PATH, rootPkg); + + for (const example of examples) { + const p = examplePkgPath(example); + const pkg = await readJson(p); + let mutated = false; + for (const k of ["dependencies", "devDependencies"] as const) { + const deps = pkg[k]; + if (!deps) continue; + for (const [n, v] of Object.entries(deps)) { + if (v === "workspace:*" && PUBLISHED.some((pp) => pp.name === n)) { + deps[n] = "catalog:"; + mutated = true; + } + } + } + if (mutated) await writeJson(p, pkg); + } + + // Mirror the new catalog entries into pnpm-workspace.yaml so the + // pnpm leg sees them too. + await writePnpmWorkspace(); + + await installAll(); + }, TIMEOUT); +} + +/** + * Restore the root + example package.json files via `git checkout` and + * reinstall once. No-op when nothing has been mutated (non-canary mode). + */ +const restoreWorkspaceDeps = async () => { + if (!canary) return; + const paths = [ROOT_PKG_PATH, ...examples.map(examplePkgPath)]; + await run(["git", "checkout", "--", ...paths], ROOT); + await writePnpmWorkspace(); + for (const { cmd, env } of installCmds()) { + await run(cmd, ROOT, env); + } +}; + +// Always-on setup: write `pnpm-workspace.yaml` mirroring bun's catalog so +// `pnpm exec` works in CI (pnpm 11's deps-status check otherwise fails on +// `catalog:` deps it doesn't know about). +beforeAll(writePnpmWorkspace, TIMEOUT); + +// Always restore + clean up on a normal end-of-suite, regardless of canary +// mode. +afterAll(async () => { + await restoreWorkspaceDeps(); + await removePnpmWorkspace(); +}, TIMEOUT); + +// Also restore + clean up if the suite is interrupted (Ctrl+C / SIGTERM). +let restoring = false; +for (const sig of ["SIGINT", "SIGTERM"] as const) { + process.on(sig, () => { + if (restoring) return; + restoring = true; + Promise.allSettled([restoreWorkspaceDeps(), removePnpmWorkspace()]) + .catch((err) => console.error("teardown failed:", err)) + .finally(() => process.exit(130)); + }); +} + +// One `test.concurrent` per (example, runtime) so failures point at the +// specific runtime that broke. Examples run in parallel, but within a +// single example the runtimes are chained on a per-example promise so bun +// finishes its destroy → deploy → destroy before pnpm starts in the same +// directory (otherwise both runs race on shared build outputs like +// vite's `dist/` and `.alchemy/`). +// ──────────────────────────────────────────────────────────────────────── +// Monorepo smoke tests +// +// The monorepo examples (`monorepo-single-stack`, `monorepo-multi-stack`) +// can't be tested in-place like the flat examples above — they ship with +// a `_package.json` instead of `package.json` so the root workspace +// install doesn't try to wire them in as nested workspaces with their +// own `workspace:*` graphs. +// +// For each (monorepo, runtime) pair we: +// 1. `bun pm pack` `packages/alchemy` once → tarball +// 2. copy the example to a fresh temp dir (skipping build artifacts) +// 3. rename `_package.json` → `package.json` +// 4. rewrite every `alchemy: workspace:*` ref to `file:` +// and substitute `catalog:` refs against the root catalog +// 5. write `pnpm-workspace.yaml` when running under pnpm +// 6. install with the runtime, then run destroy → deploy → destroy +// +// `monorepo-single-stack` has one `alchemy.run.ts` at the root. +// `monorepo-multi-stack` has one per package; deploy backend → frontend, +// destroy frontend → backend. +// ──────────────────────────────────────────────────────────────────────── + +type Monorepo = { + name: "monorepo-single-stack" | "monorepo-multi-stack"; + // The directories (relative to the monorepo root) where `alchemy + // {deploy,destroy}` must run, in deploy order. Destroy runs in + // reverse. + deployDirs: readonly string[]; +}; + +const monorepos: readonly Monorepo[] = [ + { name: "monorepo-single-stack", deployDirs: ["."] }, + { name: "monorepo-multi-stack", deployDirs: ["backend", "frontend"] }, +]; + +let alchemyTarball: string | undefined; + +beforeAll(async () => { + const pkgDir = path.join(ROOT, "packages", "alchemy"); + for (const f of await fs.readdir(pkgDir)) { + if (f.endsWith(".tgz")) await fs.rm(path.join(pkgDir, f)); + } + // pnpm resolves `alchemy` via `lib/` (node import condition) so it + // must reflect current `src/`; bun pm pack does not run a build. + expect(await run(["bun", "run", "build"], pkgDir)).toBe(0); + expect(await run(["bun", "pm", "pack", "--destination", "."], pkgDir)).toBe( + 0, + ); + const tgz = (await fs.readdir(pkgDir)).find((f) => f.endsWith(".tgz")); + if (!tgz) throw new Error(`bun pm pack produced no tarball in ${pkgDir}`); + alchemyTarball = path.join(pkgDir, tgz); +}, TIMEOUT); + +const SKIP_COPY = new Set([ + "node_modules", + "dist", + ".alchemy", + ".turbo", + ".wrangler", + "tsconfig.tsbuildinfo", +]); + +const copyMonorepo = async (src: string, dst: string): Promise => { + await fs.mkdir(dst, { recursive: true }); + for (const entry of await fs.readdir(src, { withFileTypes: true })) { + if (SKIP_COPY.has(entry.name)) continue; + const s = path.join(src, entry.name); + const d = path.join(dst, entry.name); + if (entry.isDirectory()) { + await copyMonorepo(s, d); + } else if (entry.isSymbolicLink()) { + // Skip — likely a workspace symlink that won't exist in the copy. + } else { + await fs.copyFile(s, d); + } + } +}; + +const PUBLISHED_NAMES = new Set(PUBLISHED.map((p) => p.name)); + +const resolveCatalog = (rootCatalog: Record) => { + return (deps: Record | undefined): boolean => { + if (!deps) return false; + let mutated = false; + for (const [name, version] of Object.entries(deps)) { + const isPublished = PUBLISHED_NAMES.has(name); + if ( + isPublished && + (version === "workspace:*" || version === "catalog:") + ) { + // In canary mode the root catalog has been rewritten with + // pkg.ing URLs (see canary `beforeAll`); resolve through it + // so the monorepo install actually exercises the canary + // install path. Otherwise fall back to the locally-packed + // tarball for `alchemy` so monorepos still test against the + // workspace's current source on a non-canary run. + const fromCatalog = rootCatalog[name]; + if (canary && fromCatalog) { + deps[name] = fromCatalog; + } else if (name === "alchemy") { + if (!alchemyTarball) throw new Error("alchemy tarball not built"); + deps[name] = `file:${alchemyTarball}`; + } else { + throw new Error( + `dependency ${name} is "${version}" but no entry in root catalog (canary=${canary})`, + ); + } + mutated = true; + } else if (version === "catalog:") { + const resolved = rootCatalog[name]; + if (!resolved) { + throw new Error( + `dependency ${name} is "catalog:" but no entry in root catalog`, + ); + } + deps[name] = resolved; + mutated = true; + } + } + return mutated; + }; +}; + +const rewritePackageJson = async ( + pkgPath: string, + resolve: (deps: Record | undefined) => boolean, +): Promise => { + const pkg = await readJson< + Pkg & { peerDependencies?: Record } + >(pkgPath); + const a = resolve(pkg.dependencies); + const b = resolve(pkg.devDependencies); + const c = resolve(pkg.peerDependencies); + if (a || b || c) await writeJson(pkgPath, pkg); +}; + +const setupMonorepo = async ( + m: Monorepo, + runtime: Runtime, +): Promise => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), `alchemy-${m.name}-`)); + const dst = path.join(tmp, m.name); + await copyMonorepo(path.join(ROOT, "examples", m.name), dst); + + // _package.json → package.json at root + const tmpRootPkg = path.join(dst, "_package.json"); + const finalRootPkg = path.join(dst, "package.json"); + await fs.rename(tmpRootPkg, finalRootPkg); + + // Resolve `catalog:` refs using the example's own catalog first (it's + // the source of truth — `examples/monorepo-*/_package.json#workspaces.catalog`), + // falling back to the repo-root catalog for anything the example doesn't + // pin. Drop the example's `alchemy` catalog entry — it points at + // `file:../packages/alchemy` which doesn't exist in the temp checkout. + // `resolveCatalog` then rewrites `alchemy` (and other PUBLISHED refs) + // to either the canary pkg.ing URL (canary mode) or the locally-packed + // tarball (default). + const rootPkg = await readJson(ROOT_PKG_PATH); + const rootCatalog = rootPkg.workspaces?.catalog ?? {}; + const copyRootPkg = await readJson(finalRootPkg); + const exampleCatalog = copyRootPkg.workspaces?.catalog ?? {}; + delete exampleCatalog.alchemy; + if (copyRootPkg.workspaces) { + copyRootPkg.workspaces.catalog = exampleCatalog; + await writeJson(finalRootPkg, copyRootPkg); + } + const mergedCatalog: Record = { + ...rootCatalog, + ...exampleCatalog, + }; + const resolve = resolveCatalog(mergedCatalog); + + // Root may declare deps too (single-stack runs `alchemy` from the + // root, so it needs `node_modules/.bin/alchemy` hoisted there). + await rewritePackageJson(finalRootPkg, resolve); + for (const sub of ["backend", "frontend"] as const) { + await rewritePackageJson(path.join(dst, sub, "package.json"), resolve); + } + + if (runtime === "pnpm") { + // Mirror scripts/pnpm-workspace.ts — pnpm 11 fails install + // (`ERR_PNPM_IGNORED_BUILDS`) on `workerd` / `msgpackr-extract` / + // `esbuild` / `sharp` unless their build scripts are explicitly + // allowlisted. + const builds = [ + "@parcel/watcher", + "esbuild", + "msgpackr-extract", + "sharp", + "workerd", + ]; + const yaml = [ + "onlyBuiltDependencies:", + ...builds.map((n) => ` - ${JSON.stringify(n)}`), + "", + "allowBuilds:", + ...builds.map((n) => ` ${JSON.stringify(n)}: true`), + "", + "packages:", + " - backend", + " - frontend", + "", + ].join("\n"); + await fs.writeFile(path.join(dst, "pnpm-workspace.yaml"), yaml); + } + + expect( + await run( + runtime === "bun" + ? ["bun", "install"] + : ["pnpm", "install", "--no-frozen-lockfile"], + dst, + ), + ).toBe(0); + + // The frontend's Vite build resolves the backend via its `import` + // condition (`./lib/*.js`), so the workspace must be compiled + // (`tsc -b` at the root walks the project references) before any + // deploy runs. + expect( + await run( + runtime === "bun" ? ["bun", "run", "build"] : ["pnpm", "run", "build"], + dst, + ), + ).toBe(0); + + return dst; +}; + +for (const m of monorepos) { + let prev: Promise = Promise.resolve(); + for (const runtime of RUNTIMES) { + const stagePrefix = (process.env.SMOKE_STAGE ?? "smoke") + .replace(/[^a-zA-Z0-9-]/g, "-") + .toLowerCase(); + const stage = `${stagePrefix}-${runtime}-${m.name}` + .replace(/[^a-zA-Z0-9-]/g, "-") + .toLowerCase(); + const cmd = (action: "destroy" | "deploy") => + runtime === "bun" + ? ["bun", "alchemy", action, "--stage", stage, "--yes"] + : ["pnpm", "exec", "alchemy", action, "--stage", stage, "--yes"]; + + const myPrev = prev; + let release!: () => void; + prev = new Promise((r) => { + release = r; + }); + + test.concurrent( + `${m.name} (${runtime}): destroy → deploy → destroy`, + async () => { + await myPrev.catch(() => {}); + const dst = await setupMonorepo(m, runtime); + try { + const deployOrder = m.deployDirs.map((d) => path.join(dst, d)); + const destroyOrder = [...deployOrder].reverse(); + for (const d of destroyOrder) { + expect(await run(cmd("destroy"), d)).toBe(0); + } + for (const d of deployOrder) { + expect(await run(cmd("deploy"), d)).toBe(0); + } + for (const d of destroyOrder) { + expect(await run(cmd("destroy"), d)).toBe(0); + } + } finally { + release(); + // Best-effort cleanup of the temp checkout; leave it on + // failure so a developer can inspect. + await fs.rm(path.dirname(dst), { recursive: true, force: true }); + } + }, + TIMEOUT, + ); + } +} + +// Some examples model the production "shared infra + per-PR compute" +// pattern via cross-stage references (e.g. `cloudflare-neon-drizzle` +// has a `pr-*` stage that references a long-lived `staging-*` stage +// owning the Neon project). To keep tests isolated from one another, +// each test owns *both* stages — `staging-${stage}` is deployed first, +// then the main stage, then both are torn down in reverse order so the +// dependency edge (pr → staging) is respected. +// +// We only set up the pre-stage when the main stage starts with `pr-`, +// because that's the only branch in the example that crosses stages. +// Local `smoke-*` and `dev_` stages just stand up their own +// project inline and don't need the pre-stage. +const PRE_DEPLOY_STAGES: Record string[]> = { + "cloudflare-neon-drizzle": (stage) => + stage.startsWith("pr-") ? [`staging-${stage}`] : [], +}; + +for (const example of examples) { + const cwd = path.join(ROOT, "examples", example); + let prev: Promise = Promise.resolve(); + for (const runtime of RUNTIMES) { + // Prefix the stage with $SMOKE_STAGE when provided so PR runs + // (`pr--…`) and main runs (`main-…`) never collide on the same + // cloud resource. Locally falls back to a fixed `smoke` prefix. + const stagePrefix = (process.env.SMOKE_STAGE ?? "smoke") + .replace(/[^a-zA-Z0-9-]/g, "-") + .toLowerCase(); + const stage = `${stagePrefix}-${runtime}-${example}` + .replace(/[^a-zA-Z0-9-]/g, "-") + .toLowerCase(); + const cmd = (action: "destroy" | "deploy", stageOverride = stage) => + runtime === "bun" + ? ["bun", "alchemy", action, "--stage", stageOverride, "--yes"] + : [ + "pnpm", + "exec", + "alchemy", + action, + "--stage", + stageOverride, + "--yes", + ]; + const preStages = PRE_DEPLOY_STAGES[example]?.(stage) ?? []; + + const myPrev = prev; + let release!: () => void; + prev = new Promise((r) => { + release = r; + }); + + test.concurrent( + `${example} (${runtime}): destroy → deploy → destroy`, + async () => { + // Wait for the previous runtime in this example to release the + // shared working directory. `catch(() => {})` so a failed earlier + // runtime doesn't cascade-fail every later runtime — the failure + // is already attributed to the right test. + await myPrev.catch(() => {}); + try { + // Clean up any leftovers across all stages before deploying. + // Order: dependents (main) first, dependencies (pre-stages) last. + expect(await run(cmd("destroy"), cwd)).toBe(0); + for (const s of [...preStages].reverse()) { + expect(await run(cmd("destroy", s), cwd)).toBe(0); + } + // Deploy: pre-stages first (data plane), main last (compute). + for (const s of preStages) { + expect(await run(cmd("deploy", s), cwd)).toBe(0); + } + expect(await run(cmd("deploy"), cwd)).toBe(0); + // Tear down in reverse so cross-stage refs stay resolvable + // until the dependent stage is gone. + expect(await run(cmd("destroy"), cwd)).toBe(0); + for (const s of [...preStages].reverse()) { + expect(await run(cmd("destroy", s), cwd)).toBe(0); + } + } finally { + release(); + } + }, + TIMEOUT, + ); + } +} diff --git a/.repos/alchemy-effect/tsconfig.base.json b/.repos/alchemy-effect/tsconfig.base.json new file mode 100644 index 00000000000..befa21471a9 --- /dev/null +++ b/.repos/alchemy-effect/tsconfig.base.json @@ -0,0 +1,34 @@ +{ + "exclude": ["node_modules", "dist", "vendor"], + "compilerOptions": { + "types": ["bun"], + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": true, + // Bundler mode + "module": "Preserve", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "noImplicitThis": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "noErrorTruncation": true + } +} diff --git a/.repos/alchemy-effect/tsconfig.json b/.repos/alchemy-effect/tsconfig.json new file mode 100644 index 00000000000..abf5f00a2bb --- /dev/null +++ b/.repos/alchemy-effect/tsconfig.json @@ -0,0 +1,99 @@ +{ + "files": [], + "exclude": ["node_modules", "dist"], + "references": [ + { + "path": "./tsconfig.scripts.json" + }, + { + "path": "./packages/alchemy/tsconfig.json" + }, + { + "path": "./packages/alchemy/tsconfig.bin.json" + }, + { + "path": "./packages/alchemy/tsconfig.test.json" + }, + { + "path": "./packages/better-auth/tsconfig.json" + }, + { + "path": "./packages/pr-package/tsconfig.json" + }, + // TODO(sam): fix by upgrading starlight to ^0.39.2 + // { + // "path": "./website/tsconfig.json" + // }, + { + "path": "./examples/aws-ec2/tsconfig.json" + }, + { + "path": "./examples/aws-ecs/tsconfig.json" + }, + { + "path": "./examples/aws-eks/tsconfig.json" + }, + { + "path": "./examples/aws-lambda-httpapi/tsconfig.json" + }, + { + "path": "./examples/aws-rest-api/tsconfig.json" + }, + { + "path": "./examples/aws-lambda-rpc/tsconfig.json" + }, + { + "path": "./examples/aws-lambda/tsconfig.json" + }, + { + "path": "./examples/aws-rds/tsconfig.json" + }, + { + "path": "./examples/aws-static-site/tsconfig.json" + }, + { + "path": "./examples/aws-vite/tsconfig.json" + }, + { + "path": "./examples/cloudflare-static-site/tsconfig.json" + }, + { + "path": "./examples/cloudflare-tanstack/tsconfig.json" + }, + { + "path": "./examples/cloudflare-worker/tsconfig.json" + }, + { + "path": "./examples/cloudflare-email/tsconfig.json" + }, + { + "path": "./examples/cloudflare-neon-drizzle/tsconfig.json" + }, + { + "path": "./examples/cloudflare-planetscale-mysql-drizzle/tsconfig.json" + }, + { + "path": "./examples/cloudflare-planetscale-postgres-drizzle/tsconfig.json" + }, + { + "path": "./examples/cloudflare-secrets-store/tsconfig.json" + }, + { + "path": "./examples/cloudflare-worker-async/tsconfig.json" + }, + { + "path": "./examples/monorepo-single-stack/tsconfig.json" + }, + { + "path": "./examples/monorepo-multi-stack/tsconfig.json" + } + ], + "compilerOptions": { + "plugins": [ + // ... other LSPs (if any) and as last + { + "name": "@effect/language-service" + } + ] + } +} diff --git a/.repos/alchemy-effect/tsconfig.scripts.json b/.repos/alchemy-effect/tsconfig.scripts.json new file mode 100644 index 00000000000..fae3e389a48 --- /dev/null +++ b/.repos/alchemy-effect/tsconfig.scripts.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "include": ["scripts/**/*.ts"], + "compilerOptions": { + "noEmit": true, + "types": ["bun", "node"] + } +} diff --git a/.repos/alchemy-effect/vite.config.ts b/.repos/alchemy-effect/vite.config.ts new file mode 100644 index 00000000000..4354307a740 --- /dev/null +++ b/.repos/alchemy-effect/vite.config.ts @@ -0,0 +1,12 @@ +// vitest.config.ts +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [tsconfigPaths() as any], + test: { + globals: true, + environment: "node", // or 'jsdom' for frontend tests + include: ["alchemy/test/**/*.test.ts"], + }, +}); diff --git a/.repos/alchemy-effect/vitest.config.ts b/.repos/alchemy-effect/vitest.config.ts new file mode 100644 index 00000000000..9f2bbe8cad4 --- /dev/null +++ b/.repos/alchemy-effect/vitest.config.ts @@ -0,0 +1,78 @@ +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +const apiGatewayInclude = ["test/AWS/ApiGateway/**/*.test.ts"]; + +export default defineConfig({ + plugins: [tsconfigPaths({ projects: ["./tsconfig.test.json"] })], + test: { + root: "packages/alchemy", + testTimeout: 120000, + hookTimeout: 120000, + passWithNoTests: true, + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/lib/**", + "**/.{idea,git,cache,output,temp}/**", + ], + env: { NODE_ENV: "test" }, + globals: true, + setupFiles: ["test/vitest.setup.ts"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: [ + ".distilled/**", + "coverage/**", + "dist/**", + "lib/**", + "**/node_modules/**", + "**/*.test.ts", + "**/*.config.*", + ], + }, + projects: [ + { + extends: true, + test: { + name: "alchemy", + // Bun's worker_threads segfaults under vitest's "threads" pool + // (tinypool) at worker spawn. "forks" uses child_process, which + // Bun handles reliably. Tests here are network/IO-bound, so the + // per-fork startup cost is negligible. + pool: "forks", + maxWorkers: 32, + sequence: { concurrent: true }, + include: ["test/**/*.test.ts"], + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/lib/**", + "**/.{idea,git,cache,output,temp}/**", + ...apiGatewayInclude, + ], + }, + }, + { + // API Gateway has account-wide throttles that are unworkable under + // any concurrency: DeleteRestApi alone allows 1 request per 30s. + // Force a single fork, no file parallelism, and sequential tests + // within each file so the whole suite runs as a single line of + // mutations. Bump timeouts so that retry budgets covering the + // throttle window don't blow the default 120s ceiling. + extends: true, + test: { + name: "apigateway", + pool: "forks", + singleFork: true, + fileParallelism: false, + sequence: { concurrent: false }, + testTimeout: 600_000, + hookTimeout: 600_000, + include: apiGatewayInclude, + }, + }, + ], + }, +}); diff --git a/.repos/alchemy-effect/website/.gitignore b/.repos/alchemy-effect/website/.gitignore new file mode 100644 index 00000000000..27bf4323eb4 --- /dev/null +++ b/.repos/alchemy-effect/website/.gitignore @@ -0,0 +1,12 @@ +.wrangler +.astro +node_modules +.DS_Store +static/ +# public/ +src/content/docs/providers/ +.link-checker/broken-links.log +# Brand fonts vendored at build-time by scripts/download-fonts.ts. +# Cached locally; regenerated by `bun run build` if missing. +assets/fonts/*.ttf +assets/fonts/*.otf \ No newline at end of file diff --git a/.repos/alchemy-effect/website/README.md b/.repos/alchemy-effect/website/README.md new file mode 100644 index 00000000000..ced167421f9 --- /dev/null +++ b/.repos/alchemy-effect/website/README.md @@ -0,0 +1,39 @@ +# Alchemy Effect Website + +This workspace contains the customer-facing docs site for Alchemy Effect. + +## Build Pipeline + +The site is intentionally split into independent steps: + +1. `bun run build:reference` generates Zola-compatible API reference pages from the TypeScript source tree. +2. `bun run build:assets` compiles the shared Tailwind CSS and the custom browser JavaScript bundle. +3. `bun run build:site` renders the site with Zola. +4. `bun run build:search` indexes the built HTML with Pagefind. +5. `alchemy.run.ts` deploys the final `dist/` directory through `Cloudflare.StaticSite(...)`. + +This keeps the large markdown corpus on a Rust-first rendering path while still +allowing a modern custom UI. + +## Local Commands + +- `bun run build` +- `bun run dev:site` +- `bun run deploy` +- `bun run destroy` + +## Benchmark Snapshot + +Measured locally on 2026-04-02: + +- API reference generation: about `2.2s` +- Tailwind + browser bundle build: about `0.9s` +- Zola render: about `0.6s` +- Pagefind indexing: about `0.3s` +- Total built files in `dist/`: `693` +- Total built bytes in `dist/`: `3,120,670` +- Pagefind files: `350` +- Pagefind bytes: `633,383` + +These numbers should be treated as a baseline for future changes to the docs +pipeline. diff --git a/.repos/alchemy-effect/website/alchemy.run.ts b/.repos/alchemy-effect/website/alchemy.run.ts new file mode 100644 index 00000000000..642acea1257 --- /dev/null +++ b/.repos/alchemy-effect/website/alchemy.run.ts @@ -0,0 +1,84 @@ +import * as Alchemy from "alchemy"; +import * as Cloudflare from "alchemy/Cloudflare"; +import * as GitHub from "alchemy/GitHub"; +import * as Output from "alchemy/Output"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +export type WorkerEnv = Cloudflare.InferEnv; + +const Website = Cloudflare.StaticSite( + "Website", + Alchemy.Stack.useSync((stack) => ({ + command: "bun run build", + name: + stack.stage === "prod" + ? // FUCK: i deleted state lol, let's adopt this to avoid potential DNS prop issue + "alchemyeffectwebsite-worker-prod-piyvp3qw7565vvin" + : undefined, + main: "./src/worker.ts", + outdir: "dist", + domain: stack.stage === "prod" ? "v2.alchemy.run" : undefined, + memo: { + include: [ + "src/**", + "astro.config.mjs", + "package.json", + "plugins/**", + "public/**", + "scripts/**", + "../bun.lock", + ], + }, + compatibility: { + date: "2026-04-02", + flags: ["nodejs_compat"], + }, + assetsConfig: { + runWorkerFirst: true, + }, + })), +); + +export default Alchemy.Stack( + "AlchemyEffectWebsite", + { + providers: Layer.mergeAll(Cloudflare.providers(), GitHub.providers()), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const { stage } = yield* Alchemy.Stack; + const website = yield* Website; + + if (stage.startsWith("pr-")) { + yield* GitHub.Comment("preview-comment", { + owner: "alchemy-run", + repository: "alchemy-effect", + issueNumber: Number(process.env.PULL_REQUEST), + body: Output.interpolate` + ## Website Preview Deployed + + **URL:** ${website.url} + + Built from commit ${ + // `BUILD_SHA` is set by .github/workflows/deploy.yml to the + // PR head SHA (or `github.sha` for push deploys). The + // ambient `GITHUB_SHA` would point at the synthetic merge + // commit on `pull_request` events, which is not what + // anyone wants to see in the comment. + process.env.BUILD_SHA + ? `[\`${process.env.BUILD_SHA.slice(0, 7)}\`](https://github.com/alchemy-run/alchemy-effect/commit/${process.env.BUILD_SHA})` + : "unknown" + }. + + --- + _This comment updates automatically with each push._ + `, + }); + } + + return { + url: website.url, + }; + }), +); diff --git a/.repos/alchemy-effect/website/astro.config.mjs b/.repos/alchemy-effect/website/astro.config.mjs new file mode 100644 index 00000000000..6d8746940d7 --- /dev/null +++ b/.repos/alchemy-effect/website/astro.config.mjs @@ -0,0 +1,249 @@ +// @ts-check +import mdx from "@astrojs/mdx"; +import react from "@astrojs/react"; +import sitemap from "@astrojs/sitemap"; +import starlight from "@astrojs/starlight"; +import tailwindcss from "@tailwindcss/vite"; +import astroBrokenLinksChecker from "astro-broken-links-checker"; +import { defineConfig } from "astro/config"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import starlightBlog from "starlight-blog"; +import { pagefindIgnoreNoise } from "./plugins/pagefind-ignore-noise.mjs"; + +/** + * Copies `src/content/docs/**\/*.{md,mdx}` into the build output dir, preserving + * the directory layout but normalizing extensions to `.md`. This lets the worker + * serve raw markdown for clients (e.g. coding agents) that prefer it. + * + * @returns {import("astro").AstroIntegration} + */ +function copyMarkdownSources() { + return { + name: "copy-markdown-sources", + hooks: { + "astro:build:done": async ({ dir }) => { + const outDir = fileURLToPath(dir); + + /** + * @param {string} srcDir + * @param {{ lowercase?: boolean }} [opts] + * @param {string} [relTo] + */ + async function walk(srcDir, opts = {}, relTo = srcDir) { + let entries; + try { + entries = await fs.readdir(srcDir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const full = path.join(srcDir, entry.name); + if (entry.isDirectory()) { + await walk(full, opts, relTo); + continue; + } + if (!entry.isFile()) continue; + const ext = path.extname(entry.name).toLowerCase(); + if (ext !== ".md" && ext !== ".mdx") continue; + let rel = path.relative(relTo, full); + rel = rel.slice(0, rel.length - ext.length) + ".md"; + // Starlight lowercases doc URLs (e.g. CamelCase source + // `providers/AWS/S3/Bucket.md` is served at `/providers/aws/s3/bucket`), + // so the raw-markdown copy must live at the lowercased path or the + // worker's `/providers/aws/s3/bucket.md` lookup 404s into HTML. + if (opts.lowercase) rel = rel.toLowerCase(); + const target = path.join(outDir, rel); + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.copyFile(full, target); + } + } + + // Docs (Starlight content collection) — preserves nested layout under + // /content/docs/ → /.md, lowercased to match Starlight's URLs. + await walk( + fileURLToPath(new URL("./src/content/docs/", import.meta.url)), + { lowercase: true }, + ); + // Marketing pages (top-level Astro pages) — exposes /.md so + // agents can fetch raw MDX via the worker's content negotiation. Astro + // page routing preserves case, so don't lowercase these. + await walk(fileURLToPath(new URL("./src/pages/", import.meta.url))); + }, + }, + }; +} + +/** + * Case-sensitive internal-link checker. astro-broken-links-checker uses + * `fs.existsSync`, which is case-insensitive on macOS — so `/foo/Bar` will + * resolve to `/foo/bar` locally but 404 on Linux CI. This integration walks + * the build output once into a case-sensitive Set of paths and validates + * every `href`/`src` against it. + * + * @returns {import("astro").AstroIntegration} + */ +function caseSensitiveLinkChecker() { + return { + name: "case-sensitive-link-checker", + hooks: { + "astro:build:done": async ({ dir, logger }) => { + const distPath = fileURLToPath(dir); + + /** @type {Set} */ + const paths = new Set(); + /** @type {Set} */ + const dirs = new Set(); + /** + * @param {string} d + */ + async function walk(d) { + const entries = await fs.readdir(d, { withFileTypes: true }); + for (const entry of entries) { + const full = path.join(d, entry.name); + if (entry.isDirectory()) { + dirs.add("/" + path.relative(distPath, full)); + await walk(full); + } else if (entry.isFile()) { + paths.add("/" + path.relative(distPath, full)); + } + } + } + await walk(distPath); + + /** @type {Map>} */ + const broken = new Map(); + const htmlFiles = [...paths].filter((p) => p.endsWith(".html")); + + for (const htmlFile of htmlFiles) { + const html = await fs.readFile( + path.join(distPath, htmlFile.slice(1)), + "utf8", + ); + const links = [ + ...html.matchAll(/]*href="([^"#?]+)/gi), + ...html.matchAll(/]*src="([^"#?]+)/gi), + ].map((m) => m[1]); + + for (const link of links) { + if (!link.startsWith("/")) continue; // skip external, anchors, mailto, etc. + const clean = link.replace(/\/$/, ""); + const fileCandidates = [ + clean, + clean + "/index.html", + clean + ".html", + ]; + const exists = + fileCandidates.some((c) => paths.has(c)) || dirs.has(clean); + if (!exists) { + if (!broken.has(link)) broken.set(link, new Set()); + broken.get(link)?.add(htmlFile); + } + } + } + + if (broken.size > 0) { + let msg = "Case-sensitive broken links detected:\n"; + for (const [link, docs] of broken.entries()) { + msg += `\n ${link}\n Found in:\n`; + for (const doc of docs) msg += ` - ${doc}\n`; + } + logger.error(msg); + throw new Error( + `Case-sensitive broken links detected (${broken.size})`, + ); + } + logger.info( + `Case-sensitive link check passed (${htmlFiles.length} pages)`, + ); + }, + }, + }; +} + +export default defineConfig({ + site: "https://v2.alchemy.run", + prefetch: true, + trailingSlash: "ignore", + integrations: [ + react(), + pagefindIgnoreNoise(), + copyMarkdownSources(), + astroBrokenLinksChecker({ + checkExternalLinks: false, + throwError: true, + }), + caseSensitiveLinkChecker(), + sitemap({ + filter: (page) => + !page.endsWith(".html") && + !page.endsWith(".md") && + !page.endsWith(".mdx"), + }), + starlight({ + title: "alchemy", + favicon: "/favicon.svg", + customCss: ["./src/styles/global.css", "./src/styles/custom.css"], + components: { + ThemeProvider: "./src/components/ThemeProvider.astro", + Header: "./src/components/marketing/Nav.astro", + Head: "./src/components/starlight/Head.astro", + }, + prerender: true, + social: [ + { + icon: "github", + label: "GitHub", + href: "https://github.com/alchemy-run/alchemy-effect", + }, + ], + editLink: { + baseUrl: + "https://github.com/alchemy-run/alchemy-effect/edit/main/website", + }, + sidebar: [ + { label: "What is Alchemy?", link: "/what-is-alchemy" }, + { label: "Getting Started", link: "/getting-started" }, + { + label: "Tutorial", + items: [ + { label: "Part 1: Your First Stack", link: "/tutorial/part-1" }, + { label: "Part 2: Add a Worker", link: "/tutorial/part-2" }, + { label: "Part 3: Testing", link: "/tutorial/part-3" }, + { label: "Part 4: Local Dev", link: "/tutorial/part-4" }, + { label: "Part 5: CI/CD", link: "/tutorial/part-5" }, + { + label: "Cloudflare", + autogenerate: { directory: "tutorial/cloudflare" }, + collapsed: true, + }, + { + label: "AWS", + autogenerate: { directory: "tutorial/aws" }, + collapsed: true, + }, + ], + }, + { + label: "Concepts", + autogenerate: { directory: "concepts" }, + }, + { + label: "Guides", + autogenerate: { directory: "guides" }, + }, + { + label: "Providers", + autogenerate: { directory: "providers", collapsed: true }, + }, + ], + plugins: [starlightBlog()], + routeMiddleware: ["./src/blog-sidebar.ts"], + }), + mdx(), + ], + vite: { + plugins: [tailwindcss()], + }, +}); diff --git a/.repos/alchemy-effect/website/design-system/README.md b/.repos/alchemy-effect/website/design-system/README.md new file mode 100644 index 00000000000..6ba3dde280b --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/README.md @@ -0,0 +1,316 @@ +# Alchemy Design System + +> **Infrastructure-as-Effects** — a TypeScript framework that unifies cloud +> infrastructure and application logic into a single type-safe program +> powered by [Effect](https://effect.website). + +Alchemy is a developer-tool brand: terminal-first, code-dense, dark-mode only, +with one signature color — mint green `#00e599`. Marketing moments lean on +**hand-drawn sketch diagrams** (arrows, circles, scribbled labels) to explain +abstract type-system concepts. Everything else is flat, quiet, and close to +black. + +This design system captures the visuals, tone, and components needed to ship +new marketing pages, docs, social posts, and slide decks that feel like +Alchemy — without re-inventing tokens each time. + +--- + +## Sources + +- **Repo:** `github.com/alchemy-run/alchemy-effect` — website (Astro + + Starlight), README, docs content, raw sketch PNGs under `images/`. +- **Live docs:** https://alchemy.run +- **Docs styling:** `website/src/styles/custom.css` — the ground truth for + color tokens, spacing, dark-only theme. +- **Hero diagrams:** `website/images/alchemy-effect-*.png` — hand-drawn + Function → Binding → Resource triple, layers, terminal screenshots. +- **Sibling repos** (same brand): `alchemy-run/alchemy` (core), + `alchemy-run/distilled` (Effect-native cloud SDKs). + +--- + +## Index + +| File / folder | What's in it | +| --------------------- | ------------------------------------------------------------------------------- | +| `README.md` | This doc — brand context, content & visual foundations, iconography | +| `SKILL.md` | Agent Skill manifest (usable standalone in Claude Code) | +| `colors_and_type.css` | All design tokens: colors, type, spacing, radii, shadows, motion | +| `fonts/` | Local webfont fallbacks (Inter, JetBrains Mono, Caveat via Google Fonts import) | +| `assets/` | Logos, hand-drawn diagrams, product screenshots | +| `preview/` | HTML specimen cards — one per token group / component cluster | +| `ui_kits/website/` | Marketing + docs UI kit (hero, feature grid, terminal, provider cards) | +| `ui_kits/docs/` | Docs reader UI kit (sidebar nav, article, code blocks, callouts) | + +### Products covered + +1. **alchemy.run marketing site** — landing, what-is, getting-started. Astro + Starlight. +2. **alchemy.run docs** — sidebar nav, MDX articles, expressive-code blocks, terminal widget. + +No app UI exists — Alchemy is a CLI + library. The CLI's terminal output +(`$ alchemy deploy`, colored plan/apply reports) is itself a visual surface +and is recreated as a component. + +--- + +## Content Fundamentals + +Alchemy's voice is **technical, calm, and quietly opinionated**. It talks to +senior TypeScript engineers. It never hypes, never uses exclamation marks for +emphasis, and never uses emoji outside the occasional Discord context. + +**Tone rules** + +- **Lowercase brand name** always: _alchemy_ — not "Alchemy" in body copy + (headings can capitalize for sentence case; the wordmark itself is always + lowercase). +- **Short declarative sentences.** Max ~18 words. Break into paragraphs + instead of running on. +- **"You" for the reader, "we" for the team.** "Come hang in our Discord." + "You'll install Alchemy and Effect." +- **Technical terms are the nouns.** Capitalize product concepts when used + as proper nouns: _Stack_, _Resource_, _Provider_, _Binding_, _Layer_, + _Output Attribute_. Leave verbs lowercase: _deploy_, _bind_, _yield_. +- **Code is a first-class citizen.** Almost every paragraph either + introduces a code block or refers to `monospace identifiers`. Don't + paraphrase what the code shows — show it. +- **No hype adjectives.** Never say "amazing", "powerful", "revolutionary", + "magical". Say what it does. +- **Specific numbers > vague claims.** "in under two minutes", "Node.js 22+", + "under 30 minutes" — concrete, verifiable. +- **Em-dashes (—) for rhetorical pivots**; used frequently. Same character, + no spaces around it sometimes, with spaces sometimes — match the docs. +- **Tagline form:** "**X.** Y." — two sentences, first is the category, second + is the value. _"Infrastructure-as-Effects. Your infrastructure and + application logic in a single, type-safe program."_ + +**Casing** + +- Product name in prose: **alchemy** (lowercase) +- Headings: **Sentence case** — "Getting started", "What is alchemy?", + "Plan, deploy, destroy" +- `alchemy deploy`, `alchemy dev`, `alchemy destroy` — CLI verbs lowercase +- TypeScript identifiers verbatim (`Cloudflare.R2Bucket`, `Effect.gen`) + +**Don't** + +- ❌ Emoji in marketing / docs body copy +- ❌ Rhetorical questions as headings ("Why Alchemy?") +- ❌ Marketing filler ("we're excited to announce…") +- ❌ Capitalizing "Alchemy" mid-sentence +- ❌ Introducing a concept without a code example nearby + +**Representative copy** + +> _"Infrastructure as **Effects**. Your infrastructure and application logic in +> a single, type-safe program."_ +> +> _"If it compiles, it deploys."_ +> +> _"Resources are just Effects. Resources are declared as Effects and +> composed with `yield_`. Import them from any file, bind them to Workers, +> pass their outputs to other resources — it's all just TypeScript."\* +> +> _"Preview what will change with `plan`, apply it with `deploy`, and tear +> it down with `destroy`. Stages isolate environments so `dev` and `prod` +> never collide."_ +> +> _"alchemy is in alpha and not ready for production use (expect breaking +> changes). Come hang in our Discord to participate in the early stages of +> development."_ + +--- + +## Visual Foundations + +### Mode + +**Dark mode only.** The `color-scheme: dark` declaration is hard-coded and +the theme toggle is hidden. Every surface is on a near-black canvas. Do +not produce light-mode variants unless explicitly asked — they don't exist. + +### Color + +- **One accent:** `#00e599` — a bright, slightly-yellow mint green. Used for + emphasis, success states, CTAs, link hovers, the wordmark dot, and the + single gradient stop in "Infrastructure as **Effects**" hero text. +- **Canvas:** `#0a0a0a` (page) → `#111111` (nav/sidebar) → `#18181b` (elevated + cards) → `#1f1f23` (hover). Steps are ~1–2 lightness units apart — very + subtle. +- **Neutrals:** Tailwind Zinc scale (50 → 950). Body text at zinc-200/300, + muted at zinc-400, captions at zinc-500. +- **Hairlines:** `rgba(255,255,255,0.06)` for default borders, `0.10` for + hover/emphasis. No solid-gray borders. +- **Semantic colors in terminal output:** + - Success / create: `#00e599` (same mint) + - Update: `#f5a524` (amber) + - Replace / destroy: `#f04b4b` (red) + - Info / tag: `#7cc5ff` (cyan) + - Dim: `#71717a` + +**No purple. No blue gradients. No "SaaS violet".** The only gradient in the +entire brand is `linear-gradient(90deg, #fff, #00e599)` applied as +`background-clip: text` on the word "Effects" in the hero. + +### Type + +- **Inter** (400/500/600/700/800) — UI, headings, body +- **JetBrains Mono** (400/500/600) — all code, terminal output, eyebrows +- **Caveat** (600) — hand-drawn diagram labels (see Iconography) +- Letter-spacing: `-0.04em` on display, `-0.02em` on headings, `0` on body. +- Headings are **sentence case**, weight 700, tightly tracked. No all-caps + except monospace eyebrows (`EYEBROW TEXT`, 12px, `letter-spacing: 0.1em`). + +### Spacing + +- 4px grid: 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 / 96. +- Section rhythm on marketing pages: `margin-bottom: 5rem` (80px) between + hero / features / providers / CTA. +- Max content width on landing: `72rem` (1152px). + +### Backgrounds + +- **Flat solid black-ish surfaces** — no patterns, no noise, no gradients. +- **Hand-drawn sketch illustrations** are the signature decorative element — + they appear as standalone hero artwork, not backgrounds. See Iconography. +- Occasional full-bleed terminal screenshots demonstrating CLI output. + +### Borders & cards + +- Border radius: **8px** standard (provider cards, buttons, code blocks), + **4–6px** for small chips/tags, **12px** for large hero panels. +- Cards: `background: #18181b; border: 1px solid rgba(255,255,255,0.06); +border-radius: 8px; padding: 1.5rem;` +- **Never** a colored left-border accent. Never an "alert callout" stripe. +- Hover on a card: border becomes `var(--alc-accent)`. That's it — no + transform, no shadow change, no scale. + +### Shadows + +- Near-zero by default. Dark surfaces don't need elevation shadows. +- When used: `0 4px 14px rgba(0,0,0,0.5)` for floating elements. +- **Accent glow** is allowed sparingly on focused/active CTA buttons: + `0 0 0 1px rgba(0,229,153,.4), 0 0 24px -4px rgba(0,229,153,.4)`. + +### Transparency & blur + +- Transparency is used for **hairlines** (`rgba(255,255,255,.06–.16)`) and + **accent washes** (`color-mix(in srgb, #00e599 12%, transparent)`). +- No frosted-glass / backdrop-blur surfaces. Alchemy's surfaces are crisp + and opaque. + +### Motion + +- **Restrained.** Marketing page has essentially no animation. Hover + transitions are `120–180ms ease` on color/border only. +- Easing: `cubic-bezier(0.2, 0, 0, 1)` — standard Material out-curve. +- No bounces, no parallax, no auto-playing hero videos. +- Prefer instant state changes for developers — they move fast and dislike + jank. + +### Hover states + +- **Links:** color → `var(--alc-accent)`. +- **Cards with a link:** border-color → `var(--alc-accent)`. No translate. +- **Primary buttons:** background slightly brighter, no transform. +- **Secondary buttons / icon buttons:** background → `rgba(255,255,255,.04)`. + +### Press / active states + +- Buttons darken a touch (`filter: brightness(0.95)`) — no scale-down, no + inset shadow. The goal is "it registered" without bouncing. + +### Focus states + +- **2px mint outline, 2px offset.** Never remove focus rings. + `outline: 2px solid #00e599; outline-offset: 2px;` + +### Layout rules + +- Fixed top nav (64px tall, `#111` bg, hairline bottom border). +- Docs: 260px left sidebar (`#111`), article body max-width 768–820px, + optional right-side "On this page" column. +- Marketing: centered, 72rem max-width, generous 5rem between sections. + +### Imagery vibe + +- **Hand-drawn, warm, slightly-silly sketches** on otherwise austere dark + surfaces. Black ink on white paper, dropped into the dark theme as-is + (white backgrounds show through — it's a deliberate contrast). +- Product screenshots (VS Code, terminal) appear in their native + github-dark-dimmed theme. Preserve as PNGs. +- No stock photography. No AI-generated imagery. No people. + +### Code blocks + +- **Theme:** `github-dark-dimmed` (Expressive Code). Keyword `#f47067`, string + `#96d0ff`, function `#dcbdfb`, type `#6cb6ff`, comment `#768390`. +- **Filename header:** small mono label at top-left, dim. +- **Diff additions:** green `+` prefix with row tint; deletions: red `-` + prefix with row tint. TwoSlash-powered. + +--- + +## Iconography + +**Alchemy does not use an icon font or lucide/heroicons in its marketing +or docs.** The visual vocabulary splits into three categories: + +### 1. Hand-drawn sketch illustrations (signature) + +Black-ink marker sketches on white, scanned and dropped into the dark +theme. Used for explaining concepts on the README and marketing site. They +replace what would normally be iconographic diagrams. + +Available in `assets/`: + +- `diagram-triple.png` — **Function → Binding → Resource** (the core triple) +- `diagram-triad.png` — triad diagram +- `diagram-layers.png` — Effect Layer hierarchy sketch +- `screenshot-plan-type.png` — VS Code type-hover screenshot +- `screenshot-output.png` — VS Code stack output screenshot +- `screenshot-policy-error.png` — VS Code IAM policy type error + +**Typography in sketches:** labels look like Caveat / marker handwriting. +When recreating digitally, use **Caveat 600** as the nearest Google Fonts +match. Flag to the user that true sketches should be produced by hand or +with an illustrator. + +### 2. Code as decoration + +The biggest "icons" on the marketing site are **code blocks themselves**. +Every feature card has a syntax-highlighted TypeScript snippet paired with +a short paragraph. Treat code blocks as the primary visual unit — size +them, pad them, give them room. + +### 3. CLI glyphs (terminal component) + +Inside the custom `` component, plain unicode / ASCII glyphs +signal status: + +- `✓` success (mint) +- `+` create (mint) +- `~` update (amber) +- `-` / `×` destroy (red) +- `◉` / `○` radio selection +- `•` bullet / separator (dim) +- `[u]…[/u]` underline markup, `[b]…[/b]` bold, `[d]…[/d]` dim, `[g]…[/g]` + green/success, `[c]…[/c]` cyan + +### 4. Small UI icons (Starlight built-ins) + +The docs layout uses Starlight's stock icons — `right-arrow`, `open-book`, +`github`, `bun`, `npm`, `pnpm`, `seti:yarn` — for link buttons and tabs. +When recreating, **use Lucide** (CDN) as the closest stroke-weight match, +or the original Starlight icons if available. Flag this substitution. + +### 5. Logo mark + +`assets/logo-mark.svg` — a mint dot on a rounded black square. This is a +**placeholder** synthesized from the wordmark style on the live site; the +repo does not contain an official logo file. **Ask the user for a real +logo asset.** + +Emoji usage: **none** in marketing/docs. Fine in community spaces +(Discord) but out of scope for the design system. diff --git a/.repos/alchemy-effect/website/design-system/assets/diagram-layers.png b/.repos/alchemy-effect/website/design-system/assets/diagram-layers.png new file mode 100644 index 00000000000..01051ed5719 Binary files /dev/null and b/.repos/alchemy-effect/website/design-system/assets/diagram-layers.png differ diff --git a/.repos/alchemy-effect/website/design-system/assets/diagram-triad.png b/.repos/alchemy-effect/website/design-system/assets/diagram-triad.png new file mode 100644 index 00000000000..d82b2c53fcd Binary files /dev/null and b/.repos/alchemy-effect/website/design-system/assets/diagram-triad.png differ diff --git a/.repos/alchemy-effect/website/design-system/assets/diagram-triple.png b/.repos/alchemy-effect/website/design-system/assets/diagram-triple.png new file mode 100644 index 00000000000..f791fa78e0b Binary files /dev/null and b/.repos/alchemy-effect/website/design-system/assets/diagram-triple.png differ diff --git a/.repos/alchemy-effect/website/design-system/assets/logo-mark.svg b/.repos/alchemy-effect/website/design-system/assets/logo-mark.svg new file mode 100644 index 00000000000..337eb038d3d --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/assets/logo-mark.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.repos/alchemy-effect/website/design-system/assets/logo-wordmark-dark.svg b/.repos/alchemy-effect/website/design-system/assets/logo-wordmark-dark.svg new file mode 100644 index 00000000000..70fd8dc3287 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/assets/logo-wordmark-dark.svg @@ -0,0 +1,8 @@ + + + + + + + alchemy + \ No newline at end of file diff --git a/.repos/alchemy-effect/website/design-system/assets/logo-wordmark-light.svg b/.repos/alchemy-effect/website/design-system/assets/logo-wordmark-light.svg new file mode 100644 index 00000000000..af362b9d5d5 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/assets/logo-wordmark-light.svg @@ -0,0 +1,6 @@ + + + + + alchemy + \ No newline at end of file diff --git a/.repos/alchemy-effect/website/design-system/assets/screenshot-output.png b/.repos/alchemy-effect/website/design-system/assets/screenshot-output.png new file mode 100644 index 00000000000..37c805a9390 Binary files /dev/null and b/.repos/alchemy-effect/website/design-system/assets/screenshot-output.png differ diff --git a/.repos/alchemy-effect/website/design-system/assets/screenshot-plan-type.png b/.repos/alchemy-effect/website/design-system/assets/screenshot-plan-type.png new file mode 100644 index 00000000000..c6f94608694 Binary files /dev/null and b/.repos/alchemy-effect/website/design-system/assets/screenshot-plan-type.png differ diff --git a/.repos/alchemy-effect/website/design-system/assets/screenshot-policy-error.png b/.repos/alchemy-effect/website/design-system/assets/screenshot-policy-error.png new file mode 100644 index 00000000000..80be88e5031 Binary files /dev/null and b/.repos/alchemy-effect/website/design-system/assets/screenshot-policy-error.png differ diff --git a/.repos/alchemy-effect/website/design-system/preview/_card.css b/.repos/alchemy-effect/website/design-system/preview/_card.css new file mode 100644 index 00000000000..3d9754b0d66 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/_card.css @@ -0,0 +1,22 @@ +/* Shared card harness — keep previews consistent. */ +@import url("../colors_and_type.css"); + +* { + box-sizing: border-box; +} +html, +body { + margin: 0; + padding: 0; + background: var(--alc-bg); + color: var(--alc-fg-2); + font-family: var(--alc-font-sans); + -webkit-font-smoothing: antialiased; +} +body { + width: 700px; + padding: 24px; +} +.card { + width: 100%; +} diff --git a/.repos/alchemy-effect/website/design-system/preview/brand-glyphs.html b/.repos/alchemy-effect/website/design-system/preview/brand-glyphs.html new file mode 100644 index 00000000000..3e802b35a7f --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/brand-glyphs.html @@ -0,0 +1,87 @@ + + + + + Glyphs + + + + +
+
+
+
+
success
+
+
+
+
+
create
+
+
+
~
+
update
+
+
+
×
+
destroy
+
+
+
+
selected
+
+
+
+
radio
+
+
+
+
bullet
+
+
+
+
arrow
+
+
+
+
replace
+
+
+
+
kbd
+
+
+
+
search
+
+
+
+
flow
+
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/brand-logo.html b/.repos/alchemy-effect/website/design-system/preview/brand-logo.html new file mode 100644 index 00000000000..5e44cd25ea1 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/brand-logo.html @@ -0,0 +1,84 @@ + + + + + Logo + + + + +
+
+
+
+
alchemy
+
+
wordmark on parchment — primary
+
+
+
+
alchemy
+
+
wordmark on moss — inverse
+
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/brand-sketch.html b/.repos/alchemy-effect/website/design-system/preview/brand-sketch.html new file mode 100644 index 00000000000..31a16b07571 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/brand-sketch.html @@ -0,0 +1,49 @@ + + + + + Sketch diagram + + + + +
+
+ Function → Binding → Resource +
+
+ Signature hand-drawn diagram · Caveat labels · walnut ink on parchment +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/colors-brand.html b/.repos/alchemy-effect/website/design-system/preview/colors-brand.html new file mode 100644 index 00000000000..04e0752c520 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/colors-brand.html @@ -0,0 +1,124 @@ + + + + + + + + +
+
+
+
Moss
+
+
--alc-accent
+
#5C7A3E
+
+
+
+
accent-deep
+
#3F5A2A
+
+
+
accent-warm
+
#7A9A5E
+
+
+
accent-soft
+
#D8E3C4
+
+
+
+
+
terracotta
+
#C56E3C
+
+
+
terracotta-deep
+
#9A4F27
+
+
+
terracotta-soft
+
#ECCFB8
+
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/colors-neutrals.html b/.repos/alchemy-effect/website/design-system/preview/colors-neutrals.html new file mode 100644 index 00000000000..1954742811d --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/colors-neutrals.html @@ -0,0 +1,83 @@ + + + + + + + + +
+
+
+ 50 +
+
+ 100 +
+
+ 200 +
+
+ 300 +
+
+ 400 +
+
+ 500 +
+
+ 600 +
+
+ 700 +
+
+ 800 +
+
+ 900 +
+
+ 950 +
+
+
+ Walnut scale · --alc-walnut-{50–950} +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/colors-semantic.html b/.repos/alchemy-effect/website/design-system/preview/colors-semantic.html new file mode 100644 index 00000000000..ef0f9dae7ce --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/colors-semantic.html @@ -0,0 +1,83 @@ + + + + + + + + +
+
+
+
+
plan: create
+
Success
+
#5C7A3E
+
+
+
+
plan: update
+
Warn
+
#D49A2A
+
+
+
+
plan: replace
+
Danger
+
#B3462E
+
+
+
+
info
+
Info
+
#4A7A8A
+
+
+
+
unchanged
+
Muted
+
#A89572
+
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/colors-surfaces.html b/.repos/alchemy-effect/website/design-system/preview/colors-surfaces.html new file mode 100644 index 00000000000..793c71760ac --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/colors-surfaces.html @@ -0,0 +1,75 @@ + + + + + + + + +
+
+
+ --alc-bgpage / parchment#F5EFE3 +
+
+ --alc-bg-navnav, sidebar#EFE7D6 +
+
+ --alc-bg-elev-1cards, panels#FBF6EA +
+
+ --alc-bg-elev-2hover / nested#FFFAF0 +
+
+ --alc-bg-sunkinsets#EBE3D0 +
+
+ --alc-bg-codeterminal / code#2A2620 +
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/colors-syntax.html b/.repos/alchemy-effect/website/design-system/preview/colors-syntax.html new file mode 100644 index 00000000000..9d8cd301e63 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/colors-syntax.html @@ -0,0 +1,76 @@ + + + + + Syntax + + + + +
+
+ // github-dark-dimmed
+ import * as + Cloudflare from + "alchemy/Cloudflare";
+
+ const Bucket = + Cloudflare.R2Bucket("Bucket");
+
+ export default + Alchemy.Stack("MyApp", {
+   providers: + Cloudflare.providers(),
+ }, Effect.gen(function* () {
+   const bucket = + yield* Bucket;
+   return { url: + bucket.bucketName };
+ })); +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/components-buttons.html b/.repos/alchemy-effect/website/design-system/preview/components-buttons.html new file mode 100644 index 00000000000..ad5bc3efeb3 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/components-buttons.html @@ -0,0 +1,99 @@ + + + + + Buttons + + + + +
+ +
+
secondary
+ Tutorial + View on GitHub +
+
+
ghost
+ Learn more + Docs +
+
+
danger
+ Destroy stack +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/components-callouts.html b/.repos/alchemy-effect/website/design-system/preview/components-callouts.html new file mode 100644 index 00000000000..5ba2e178b57 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/components-callouts.html @@ -0,0 +1,80 @@ + + + + + Callouts + + + + +
+
+ + Tip. We recommend Bun for the best development + experience, but Node.js works too. +
+
+ i + Note. By convention, Alchemy looks for + alchemy.run.ts + at the root of your project. +
+
+ ! + Alpha. alchemy is in alpha and not ready for + production use. Expect breaking changes. +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/components-code-block.html b/.repos/alchemy-effect/website/design-system/preview/components-code-block.html new file mode 100644 index 00000000000..7fe408fe0a8 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/components-code-block.html @@ -0,0 +1,75 @@ + + + + + Code block + + + + +
+
+
+ alchemy.run.tstypescript +
+
import * as Alchemy from "alchemy";
+import * as Cloudflare from "alchemy/Cloudflare";
+
+export default Alchemy.Stack("MyApp", {
+  providers: Cloudflare.providers(),
+}, Effect.gen(function* () {
+  const bucket = yield* Cloudflare.R2Bucket("Bucket");
+}));
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/components-inputs.html b/.repos/alchemy-effect/website/design-system/preview/components-inputs.html new file mode 100644 index 00000000000..7b1b59e409a --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/components-inputs.html @@ -0,0 +1,98 @@ + + + + + Inputs + + + + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/components-provider-card.html b/.repos/alchemy-effect/website/design-system/preview/components-provider-card.html new file mode 100644 index 00000000000..24d2c23b138 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/components-provider-card.html @@ -0,0 +1,62 @@ + + + + + Provider card + + + + +
+
+
+ Cloudflare +

+ Workers, R2 Buckets, KV, D1, Durable Objects, Queues, Vectorize, AI +

+
+
+ AWS +

Lambda, S3, DynamoDB, SQS, Kinesis, IAM, EC2, API Gateway

+
+
+ More +

+ GitHub, Stripe, DNS, and a growing ecosystem of community providers +

+
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/components-tags.html b/.repos/alchemy-effect/website/design-system/preview/components-tags.html new file mode 100644 index 00000000000..c5cd23dd78c --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/components-tags.html @@ -0,0 +1,100 @@ + + + + + Tags / chips + + + + +
+
+
status
+ + create + ~ update + ⇄ replace + no-op +
+
+
meta
+ Cloudflare.R2Bucket + AWS.Lambda.Function + v0.1.0 +
+
+
badge
+ alpha + beta + new +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/components-terminal.html b/.repos/alchemy-effect/website/design-system/preview/components-terminal.html new file mode 100644 index 00000000000..7db41819c81 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/components-terminal.html @@ -0,0 +1,94 @@ + + + + + Terminal + + + + +
+
+
+ + + + ~/my-app +
+
$ alchemy deploy --stage prod
+
+Plan: 2 to create
+
++ Bucket (Cloudflare.R2Bucket)
++ Worker (Cloudflare.Worker) (1 bindings)
+  + Bucket
+
+ Bucket (Cloudflare.R2Bucket) created
+ Worker (Cloudflare.Worker) created
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/spacing-elevation.html b/.repos/alchemy-effect/website/design-system/preview/spacing-elevation.html new file mode 100644 index 00000000000..edfbdfac701 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/spacing-elevation.html @@ -0,0 +1,70 @@ + + + + + Elevation + + + + +
+
+
+
+
shadow-sm
+
floating chips
+
+
+
+
shadow
+
popovers, menus
+
+
+
+
glow
+
focus, active CTA
+
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/spacing-radius.html b/.repos/alchemy-effect/website/design-system/preview/spacing-radius.html new file mode 100644 index 00000000000..75636281e6e --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/spacing-radius.html @@ -0,0 +1,72 @@ + + + + + Radii + + + + +
+
+
+
+
xs
+
2
+
+
+
+
sm
+
4
+
+
+
+
base
+
8
+
+
+
+
md
+
10
+
+
+
+
lg
+
12
+
+
+
+
xl
+
16
+
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/spacing-scale.html b/.repos/alchemy-effect/website/design-system/preview/spacing-scale.html new file mode 100644 index 00000000000..25190689063 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/spacing-scale.html @@ -0,0 +1,82 @@ + + + + + Spacing scale + + + + +
+
+
space-1
+
+
4
+
+
+
space-2
+
+
8
+
+
+
space-3
+
+
12
+
+
+
space-4
+
+
16
+
+
+
space-5
+
+
24
+
+
+
space-6
+
+
32
+
+
+
space-7
+
+
48
+
+
+
space-8
+
+
64
+
+
+
space-9
+
+
96
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/type-body.html b/.repos/alchemy-effect/website/design-system/preview/type-body.html new file mode 100644 index 00000000000..0fb5f3ae39a --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/type-body.html @@ -0,0 +1,94 @@ + + + + + Body + mono + + + + +
+
+
eyebrow
+
Getting Started
+
+
+
body-lg 18
+
+ Your infrastructure and application logic in a single, type-safe + program. +
+
+
+
body 15
+
+ Resources are declared as Effects and composed with + yield*. +
+
+
+
small 13
+
+ New to Alchemy? Follow the hands-on tutorial from zero to CI/CD. +
+
+
+
mono 13
+
bun alchemy deploy --stage prod
+
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/type-display.html b/.repos/alchemy-effect/website/design-system/preview/type-display.html new file mode 100644 index 00000000000..539c1ac52d0 --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/type-display.html @@ -0,0 +1,41 @@ + + + + + Display type + + + + +
+
+ Infrastructure as Effects. +
+
+ Source Serif 4 · 500 · 72/74 · -0.015em · italic moss on emphasis +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/type-hand.html b/.repos/alchemy-effect/website/design-system/preview/type-hand.html new file mode 100644 index 00000000000..cdb1ba23bed --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/type-hand.html @@ -0,0 +1,57 @@ + + + + + Hand lettering + + + + +
+
+ Function + + Binding + + Resource +
+
+ Caveat 700 · hand-drawn diagram labels · moss on parchment +
+
+ + diff --git a/.repos/alchemy-effect/website/design-system/preview/type-headings.html b/.repos/alchemy-effect/website/design-system/preview/type-headings.html new file mode 100644 index 00000000000..d94ebc78d9a --- /dev/null +++ b/.repos/alchemy-effect/website/design-system/preview/type-headings.html @@ -0,0 +1,80 @@ + + + + + Heading scale + + + + +
+
+
H1 · Serif 500
+
Resources as Effects
+
+
+
H2 · Serif 500
+
Plan, deploy, destroy
+
+
+
H3 · Sans 600
+
Type-safe from cloud to code
+
+
+
H4 · Sans 600
+
Local dev with hot reload
+
+
+ + diff --git a/.repos/alchemy-effect/website/ec.config.mjs b/.repos/alchemy-effect/website/ec.config.mjs new file mode 100644 index 00000000000..a739446e7f5 --- /dev/null +++ b/.repos/alchemy-effect/website/ec.config.mjs @@ -0,0 +1,44 @@ +import { defineEcConfig } from "@astrojs/starlight/expressive-code"; +import ecTwoSlash from "expressive-code-twoslash"; +import { alchemyWalnutTheme } from "./plugins/alchemy-walnut-theme.mjs"; +import { capitalizedIdentifierColor } from "./plugins/capitalized-identifier-color.mjs"; +import { + twoslashDiffPrefixAnnotate, + twoslashDiffPrefixStrip, +} from "./plugins/twoslash-diff-prefix.mjs"; +import { twoslashErrorTransform } from "./plugins/twoslash-error-transform.mjs"; + +const baseUrl = new URL("../", import.meta.url).pathname; + +export default defineEcConfig({ + themes: [alchemyWalnutTheme], + plugins: [ + twoslashDiffPrefixStrip(), + ecTwoSlash({ + instanceConfigs: { + twoslash: { + explicitTrigger: true, + languages: ["ts", "tsx", "typescript"], + }, + }, + twoslashOptions: { + customTags: ["error", "warn", "log", "annotate"], + compilerOptions: { + moduleResolution: /** @type {any} */ (100), // Bundler + module: /** @type {any} */ (99), // ESNext + target: /** @type {any} */ (9), // ES2022 + strict: true, + types: ["bun"], + baseUrl, + paths: { + alchemy: ["./packages/alchemy/src/index.ts"], + "alchemy/*": ["./packages/alchemy/src/*"], + }, + }, + }, + }), + twoslashDiffPrefixAnnotate(), + twoslashErrorTransform(), + capitalizedIdentifierColor(), + ], +}); diff --git a/.repos/alchemy-effect/website/package.json b/.repos/alchemy-effect/website/package.json new file mode 100644 index 00000000000..6b33df3aab3 --- /dev/null +++ b/.repos/alchemy-effect/website/package.json @@ -0,0 +1,56 @@ +{ + "name": "website", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/alchemy-run/alchemy-effect.git", + "directory": "website" + }, + "type": "module", + "scripts": { + "build:reference": "bun ../scripts/generate-api-reference.ts", + "build:llms": "bun scripts/generate-llms-txt.ts", + "generate:readme-hero": "bun scripts/download-fonts.ts && bun scripts/generate-readme-hero.ts", + "build": "bun scripts/download-fonts.ts && bun scripts/generate-brand-assets.ts && bun run build:reference && bun run build:llms && NODE_OPTIONS=--max-old-space-size=8192 astro build", + "dev:site": "astro dev", + "deploy": "alchemy deploy", + "dev": "alchemy dev", + "destroy": "alchemy destroy", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/check": "^0.9.4", + "@astrojs/mdx": "^5.0.3", + "@astrojs/react": "^5.0.3", + "@astrojs/sitemap": "^3.5.0", + "@astrojs/starlight": "^0.34.3", + "@astrojs/starlight-tailwind": "^4.0.1", + "@effect/platform-node": "catalog:", + "@iconify-json/logos": "^1.2.11", + "@iconify-json/lucide": "^1.2.102", + "@iconify/react": "^6.0.2", + "@resvg/resvg-js": "^2.6.2", + "@tailwindcss/vite": "^4.1.14", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "alchemy": "workspace:*", + "astro": "^5.12.8", + "effect": "catalog:", + "expressive-code-twoslash": "^0.6.1", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-icons": "^5.6.0", + "rehype-mermaid": "^3.0.0", + "satori": "^0.26.0", + "starlight-blog": "0.24.0", + "tailwindcss": "^4.1.14" + }, + "devDependencies": { + "astro-broken-links-checker": "^1.1.0", + "typescript": "catalog:", + "zod": "^3" + } +} \ No newline at end of file diff --git a/.repos/alchemy-effect/website/plugins/alchemy-walnut-theme.mjs b/.repos/alchemy-effect/website/plugins/alchemy-walnut-theme.mjs new file mode 100644 index 00000000000..dbca295d47f --- /dev/null +++ b/.repos/alchemy-effect/website/plugins/alchemy-walnut-theme.mjs @@ -0,0 +1,185 @@ +import { ExpressiveCodeTheme } from "@astrojs/starlight/expressive-code"; + +/** + * Alchemy "walnut sunrise" Expressive Code theme. + * + * Dark walnut bg (#2a2620) + sunrise syntax tokens that match the design + * system's `--alc-code-*` variables. Designed for legibility on cream + * parchment pages where every other surface is light. + */ +const walnutSunrise = { + name: "alchemy-walnut-sunrise", + type: "dark", + semanticHighlighting: true, + colors: { + "editor.background": "#2a2620", + "editor.foreground": "#faf5e3", + "editor.lineHighlightBackground": "#36302280", + "editor.selectionBackground": "#5c7a3e66", + "editorLineNumber.foreground": "#85714f", + "editorLineNumber.activeForeground": "#faf5e3", + "editorIndentGuide.background": "#4e402c", + "editorIndentGuide.activeBackground": "#68573c", + "editorBracketMatch.background": "#5c7a3e33", + "editorBracketMatch.border": "#5c7a3e", + "editorWidget.background": "#363022", + "editorWidget.border": "#4e402c", + "editorHoverWidget.background": "#363022", + "editorHoverWidget.border": "#4e402c", + "editorGroupHeader.tabsBackground": "#1a1813", + "tab.activeBackground": "#2a2620", + "tab.inactiveBackground": "#1a1813", + "tab.activeForeground": "#faf5e3", + "tab.inactiveForeground": "#a89572", + "tab.border": "#4e402c", + focusBorder: "#5c7a3e", + "scrollbarSlider.background": "#4e402c80", + "scrollbarSlider.hoverBackground": "#68573c80", + "scrollbarSlider.activeBackground": "#85714f80", + "diffEditor.insertedTextBackground": "#5c7a3e26", + "diffEditor.removedTextBackground": "#b3462e26", + }, + tokenColors: [ + { + scope: ["comment", "punctuation.definition.comment", "string.comment"], + settings: { foreground: "#b3a27a", fontStyle: "italic" }, + }, + { + scope: [ + "keyword", + "storage", + "storage.type", + "storage.modifier", + "keyword.control", + "keyword.operator.new", + "keyword.operator.expression", + "keyword.other", + ], + settings: { foreground: "#d4f26a" }, + }, + { + scope: [ + "keyword.operator", + "punctuation.separator", + "punctuation.terminator", + ], + settings: { foreground: "#c7b795" }, + }, + { + scope: ["string", "string.quoted", "punctuation.definition.string"], + settings: { foreground: "#ffe38a" }, + }, + { + scope: ["string.template", "punctuation.definition.template-expression"], + settings: { foreground: "#ffe38a" }, + }, + { + scope: ["constant.numeric", "constant.language", "constant.character"], + settings: { foreground: "#ff9a6b" }, + }, + { + scope: [ + "constant.language.boolean", + "constant.language.null", + "constant.language.undefined", + ], + settings: { foreground: "#ff9a6b" }, + }, + { + scope: [ + "entity.name.function", + "support.function", + "meta.function-call entity.name.function", + "meta.function-call.method entity.name.function", + "variable.function", + "meta.definition.method entity.name.function", + ], + settings: { foreground: "#7ddfff" }, + }, + { + scope: [ + "entity.name.type", + "entity.name.class", + "entity.name.interface", + "entity.name.namespace", + "support.type", + "support.class", + "support.other.namespace", + "support.module", + "meta.type", + ], + settings: { foreground: "#7ddfff" }, + }, + { + scope: [ + "variable.other.object", + "variable.other.readwrite.alias", + "meta.import variable.other.readwrite", + "meta.export variable.other.readwrite", + "meta.object-literal.key support.type.object", + ], + settings: { foreground: "#7ddfff" }, + }, + { + scope: ["entity.name.tag", "meta.tag entity.name.tag"], + settings: { foreground: "#ffb968" }, + }, + { + scope: ["entity.other.attribute-name"], + settings: { foreground: "#d4f26a", fontStyle: "italic" }, + }, + { + scope: [ + "variable", + "variable.other", + "variable.parameter", + "meta.definition.variable variable.other", + ], + settings: { foreground: "#faf5e3" }, + }, + { + scope: ["variable.other.constant"], + settings: { foreground: "#7ddfff" }, + }, + { + scope: ["variable.other.enummember"], + settings: { foreground: "#ff9a6b" }, + }, + { + scope: ["variable.other.property", "meta.object.member"], + settings: { foreground: "#faf5e3" }, + }, + { + scope: ["support.variable", "support.constant"], + settings: { foreground: "#7ddfff" }, + }, + { + scope: ["punctuation.section.embedded", "meta.embedded"], + settings: { foreground: "#faf5e3" }, + }, + { + scope: ["markup.heading", "markup.bold"], + settings: { foreground: "#faf5e3", fontStyle: "bold" }, + }, + { + scope: ["markup.italic"], + settings: { foreground: "#faf5e3", fontStyle: "italic" }, + }, + { + scope: ["markup.inserted", "markup.inserted.diff"], + settings: { foreground: "#8fb15e" }, + }, + { + scope: ["markup.deleted", "markup.deleted.diff"], + settings: { foreground: "#e07a5f" }, + }, + { + scope: ["invalid", "invalid.illegal"], + settings: { foreground: "#e07a5f" }, + }, + ], +}; + +export const alchemyWalnutTheme = ExpressiveCodeTheme.fromJSONString( + JSON.stringify(walnutSunrise), +); diff --git a/.repos/alchemy-effect/website/plugins/capitalized-identifier-color.mjs b/.repos/alchemy-effect/website/plugins/capitalized-identifier-color.mjs new file mode 100644 index 00000000000..17f0bb0df60 --- /dev/null +++ b/.repos/alchemy-effect/website/plugins/capitalized-identifier-color.mjs @@ -0,0 +1,87 @@ +/** + * Expressive Code plugin that paints every bare capitalized identifier + * in TS/JS code blocks with the "type cyan" used by the marketing + * highlighter (`src/components/marketing/highlightTS.ts`). + * + * Why: the TextMate TS grammar shipped with shiki does NOT tokenize + * namespace references in expression position — `Alchemy` and `Effect` + * inside `Alchemy.Stack(...)` and `Effect.gen(...)` come out as plain + * untokenized text and fall back to the editor foreground. The + * marketing landing page colors every capitalized identifier cyan, + * giving snippets a distinct "type-y" rhythm. This plugin re-applies + * that same rule to docs code blocks so syntax matches across the + * whole site. + * + * The plugin runs AFTER syntax highlighting (which adds its own + * `InlineStyleAnnotation`s with the walnut-sunrise colors), and skips + * matches that overlap an existing string or comment annotation so + * we don't recolor things like `"MyApp"` inside a string literal. + */ +import { InlineStyleAnnotation } from "@astrojs/starlight/expressive-code"; + +const TARGET_LANGS = new Set([ + "ts", + "tsx", + "typescript", + "js", + "jsx", + "javascript", + "mts", + "cts", +]); + +const CAP_IDENT_RE = /\b[A-Z][A-Za-z0-9_]*\b/g; + +const CYAN = "#7ddfff"; +const STRING_COLOR = "#ffe38a"; +const COMMENT_COLOR = "#b3a27a"; + +const eq = (a, b) => (a || "").toLowerCase() === b.toLowerCase(); + +export function capitalizedIdentifierColor() { + return { + name: "capitalized-identifier-color", + hooks: { + postprocessAnalyzedCode({ codeBlock }) { + if (!TARGET_LANGS.has(codeBlock.language)) return; + + for (const line of codeBlock.getLines()) { + // Snapshot column ranges that already belong to strings or + // comments — we don't want to recolor characters inside them + // (e.g. the `MyApp` in `Alchemy.Stack("MyApp", ...)`). + const skipRanges = []; + for (const ann of line.getAnnotations()) { + if (!ann.inlineRange) continue; + const c = ann.color; + if (eq(c, STRING_COLOR) || eq(c, COMMENT_COLOR)) { + skipRanges.push(ann.inlineRange); + } + } + + const text = line.text; + CAP_IDENT_RE.lastIndex = 0; + let m; + while ((m = CAP_IDENT_RE.exec(text)) !== null) { + const columnStart = m.index; + const columnEnd = columnStart + m[0].length; + const overlapsSkip = skipRanges.some( + (r) => columnStart < r.columnEnd && columnEnd > r.columnStart, + ); + if (overlapsSkip) continue; + + line.addAnnotation( + new InlineStyleAnnotation({ + color: CYAN, + inlineRange: { columnStart, columnEnd }, + // `normal` phase: runs after syntax highlighting + // (`earliest`), so the cyan wraps and overrides any + // walnut-sunrise color the tokenizer applied. + renderPhase: "normal", + }), + ); + } + } + }, + }, + }; +} diff --git a/.repos/alchemy-effect/website/plugins/pagefind-ignore-noise.mjs b/.repos/alchemy-effect/website/plugins/pagefind-ignore-noise.mjs new file mode 100644 index 00000000000..ae905c37ace --- /dev/null +++ b/.repos/alchemy-effect/website/plugins/pagefind-ignore-noise.mjs @@ -0,0 +1,86 @@ +// @ts-check +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Walks the build output and tags noisy elements with + * `data-pagefind-ignore` so Pagefind's excerpt picker reaches for prose + * instead of code samples, sidebar nav, TOC, page footer, etc. + * + * Must run before Starlight's own `astro:build:done` hook, which spawns + * Pagefind. Astro runs hooks in integration registration order, so this + * integration must be listed before `starlight()` in `integrations`. + * + * @returns {import("astro").AstroIntegration} + */ +export function pagefindIgnoreNoise() { + // Selectors (matched as opening-tag class strings) we want Pagefind to + // skip. We add the attribute as `data-pagefind-ignore="all"` so any + // headings inside also stop being indexed (otherwise headings inside a + // collapsed code block would still show up as sub-results). + const classMatchers = [ + // Expressive Code wraps every fenced code block in this div. + "expressive-code", + // Starlight's TOC, sidebar, page footer, pagination, header. + "right-sidebar", + "sidebar-pane", + "sl-sidebar-state-persist", + "pagination-links", + "sl-mobile-toc", + "site-search", + ]; + const classRegex = new RegExp( + `<(div|nav|aside|figure|header|footer|details|site-search)([^>]*?)class="([^"]*?\\b(?:${classMatchers.join("|")})\\b[^"]*?)"([^>]*?)>`, + "g", + ); + // Also tag every
 directly, which catches the inner code element
+  // even if Expressive Code wrapping ever changes.
+  const preRegex = /]*)?>/g;
+  // Provider docs auto-generated from JSDoc start with a
+  // "> **Source:** `src/...`" blockquote that's noise for search excerpts.
+  const sourceBlockquoteRegex =
+    /
\s*

Source:<\/strong>[\s\S]*?<\/p>\s*<\/blockquote>/g; + + /** @param {string} html */ + function rewrite(html) { + let out = html.replace(classRegex, (match, tag, pre, cls, post) => { + if (match.includes("data-pagefind-ignore")) return match; + return `<${tag}${pre}class="${cls}" data-pagefind-ignore="all"${post}>`; + }); + out = out.replace(preRegex, (match, attrs = "") => { + if (match.includes("data-pagefind-ignore")) return match; + return ``; + }); + out = out.replace(sourceBlockquoteRegex, (match) => + match.replace("

", '
'), + ); + return out; + } + + return { + name: "pagefind-ignore-noise", + hooks: { + "astro:build:done": async ({ dir }) => { + const outDir = fileURLToPath(dir); + + /** @param {string} d */ + async function walk(d) { + const entries = await fs.readdir(d, { withFileTypes: true }); + for (const e of entries) { + const full = path.join(d, e.name); + if (e.isDirectory()) { + await walk(full); + } else if (e.isFile() && e.name.endsWith(".html")) { + const before = await fs.readFile(full, "utf8"); + const after = rewrite(before); + if (after !== before) await fs.writeFile(full, after); + } + } + } + + await walk(outDir); + }, + }, + }; +} diff --git a/.repos/alchemy-effect/website/plugins/twoslash-diff-prefix.mjs b/.repos/alchemy-effect/website/plugins/twoslash-diff-prefix.mjs new file mode 100644 index 00000000000..8d6e1bb3501 --- /dev/null +++ b/.repos/alchemy-effect/website/plugins/twoslash-diff-prefix.mjs @@ -0,0 +1,103 @@ +/** + * Expressive Code plugin pair that adds `+` / `-` line-prefix support to + * twoslash code blocks, similar to ```diff lang="typescript" but with + * twoslash type-checking. + * + * Usage: + * ```typescript twoslash + * // @errors: 2345 + * import { Bucket } from "./bucket.ts"; + * + const bucket = yield* Cloudflare.R2Bucket.bind(Bucket); + * ``` + * + * How it works: + * 1. `twoslashDiffPrefixStrip` runs BEFORE `ecTwoSlash` and rewrites + * each `+ foo` / `- foo` line into ` foo ...` with a trailing tag + * comment (`/_ __ALCHEMY_DIFF_INS__ _/` or `..._DEL__...`). This + * gives the TypeScript compiler valid source while preserving a + * marker that survives twoslash's rendered output. + * 2. `twoslashDiffPrefixAnnotate` runs AFTER `ecTwoSlash` has replaced + * each block line with its rendered twoslash output. It scans the + * rendered lines for the tag comment, strips it, and attaches a + * full-line `highlight ins` / `highlight del` class annotation + * using the same CSS that `@expressive-code/plugin-text-markers` + * ships. + */ +import { ExpressiveCodeAnnotation } from "@astrojs/starlight/expressive-code"; +import { addClassName } from "@astrojs/starlight/expressive-code/hast"; + +const INS_TAG = "/* __ALCHEMY_DIFF_INS__ */"; +const DEL_TAG = "/* __ALCHEMY_DIFF_DEL__ */"; + +const isTwoslash = (codeBlock) => /\btwoslash\b/.test(codeBlock.meta); + +class DiffMarkerAnnotation extends ExpressiveCodeAnnotation { + constructor(markerType) { + super({}); + this.markerType = markerType; + } + render({ nodesToTransform }) { + return nodesToTransform.map((node) => { + if (node.type === "element") { + addClassName(node, "highlight"); + addClassName(node, this.markerType); + } + return node; + }); + } +} + +/** Register BEFORE `ecTwoSlash(...)` in the Expressive Code plugins array. */ +export function twoslashDiffPrefixStrip() { + return { + name: "twoslash-diff-prefix-strip", + hooks: { + preprocessCode({ codeBlock }) { + if (!isTwoslash(codeBlock)) return; + for (const line of codeBlock.getLines()) { + // Match a `+` or `-` at column 0 (but not `++`/`--`/`+-`/`-+`). + // If followed by a space (or end of line), replace the marker with + // a single space so column alignment of the rest of the line is + // preserved exactly as authored. If followed by a non-space + // character (e.g. `+import ...`), drop the marker entirely so + // top-level statements stay flush-left. + const match = line.text.match(/^([+-])(?![+-])(.*)$/); + if (!match) continue; + const [, marker, rest] = match; + const tag = marker === "+" ? INS_TAG : DEL_TAG; + const replacement = + rest.length === 0 || rest.startsWith(" ") ? ` ${rest}` : rest; + line.editText(0, line.text.length, `${replacement} ${tag}`); + } + }, + }, + }; +} + +/** Register AFTER `ecTwoSlash(...)` in the Expressive Code plugins array. */ +export function twoslashDiffPrefixAnnotate() { + return { + name: "twoslash-diff-prefix-annotate", + hooks: { + preprocessCode({ codeBlock }) { + if (!isTwoslash(codeBlock)) return; + for (const line of codeBlock.getLines()) { + const text = line.text; + let markerType; + let tagLength; + if (text.endsWith(` ${INS_TAG}`)) { + markerType = "ins"; + tagLength = INS_TAG.length + 1; // +1 for leading space + } else if (text.endsWith(` ${DEL_TAG}`)) { + markerType = "del"; + tagLength = DEL_TAG.length + 1; + } else { + continue; + } + line.editText(text.length - tagLength, text.length, ""); + line.addAnnotation(new DiffMarkerAnnotation(markerType)); + } + }, + }, + }; +} diff --git a/.repos/alchemy-effect/website/plugins/twoslash-error-transform.mjs b/.repos/alchemy-effect/website/plugins/twoslash-error-transform.mjs new file mode 100644 index 00000000000..ffa2d161dd7 --- /dev/null +++ b/.repos/alchemy-effect/website/plugins/twoslash-error-transform.mjs @@ -0,0 +1,42 @@ +/** + * Expressive Code plugin that transforms twoslash error text per code block. + * + * Use `errorReplace` in the code block meta to specify regex replacements: + * errorReplace="pattern::replacement" + * + * Patterns are JavaScript regexes applied with the dotAll flag (. matches newlines). + * Must be the last attribute in the meta string. + * Use double quotes when the value contains single quotes, or vice versa. + */ +export function twoslashErrorTransform() { + return { + name: "twoslash-error-transform", + hooks: { + postprocessAnnotations({ codeBlock }) { + const meta = codeBlock.meta; + const match = + meta.match(/errorReplace="(.*)"\s*$/) || + meta.match(/errorReplace='(.*)'\s*$/); + if (!match) return; + + const raw = match[1]; + const sepIndex = raw.indexOf("::"); + if (sepIndex === -1) return; + + const pattern = new RegExp(raw.slice(0, sepIndex), "s"); + const replacement = raw.slice(sepIndex + 2); + + for (const line of codeBlock.getLines()) { + for (const annotation of line.getAnnotations()) { + if (annotation.name === "twoslash-error-box") { + annotation.error.text = annotation.error.text.replace( + pattern, + replacement, + ); + } + } + } + }, + }, + }; +} diff --git a/.repos/alchemy-effect/website/probe-shiki.mjs b/.repos/alchemy-effect/website/probe-shiki.mjs new file mode 100644 index 00000000000..9f844999a3b --- /dev/null +++ b/.repos/alchemy-effect/website/probe-shiki.mjs @@ -0,0 +1,31 @@ +import { createHighlighter } from "shiki/index.mjs"; +import { createOnigurumaEngine } from "shiki/engine/oniguruma"; +const hl = await createHighlighter({ + themes: ["github-dark"], + langs: ["typescript"], + engine: await createOnigurumaEngine(import("shiki/wasm")), +}); +const code = `import * as Alchemy from "alchemy"; +import * as Effect from "effect/Effect"; +export default Alchemy.Stack( + "MyApp", + {}, + Effect.gen(function* () { + yield* Photos; + }), +);`; +const tokens = hl.codeToTokensBase(code, { + lang: "typescript", + theme: "github-dark", + includeExplanation: "scopeName", +}); +for (const line of tokens) { + for (const t of line) { + if (!t.content.trim()) continue; + const scopes = (t.explanation?.[0]?.scopes ?? []) + .map((s) => s.scopeName) + .join(" → "); + console.log(JSON.stringify(t.content).padEnd(14), scopes); + } + console.log("---"); +} diff --git a/.repos/alchemy-effect/website/public/apple-touch-icon.png b/.repos/alchemy-effect/website/public/apple-touch-icon.png new file mode 100644 index 00000000000..82cb3b24ecf Binary files /dev/null and b/.repos/alchemy-effect/website/public/apple-touch-icon.png differ diff --git a/.repos/alchemy-effect/website/public/bun-logo.svg b/.repos/alchemy-effect/website/public/bun-logo.svg new file mode 100644 index 00000000000..7ef15001d2d --- /dev/null +++ b/.repos/alchemy-effect/website/public/bun-logo.svg @@ -0,0 +1 @@ +Bun Logo \ No newline at end of file diff --git a/.repos/alchemy-effect/website/public/favicon-16.png b/.repos/alchemy-effect/website/public/favicon-16.png new file mode 100644 index 00000000000..c77f40d0254 Binary files /dev/null and b/.repos/alchemy-effect/website/public/favicon-16.png differ diff --git a/.repos/alchemy-effect/website/public/favicon-32.png b/.repos/alchemy-effect/website/public/favicon-32.png new file mode 100644 index 00000000000..d76ac911a18 Binary files /dev/null and b/.repos/alchemy-effect/website/public/favicon-32.png differ diff --git a/.repos/alchemy-effect/website/public/favicon.png b/.repos/alchemy-effect/website/public/favicon.png new file mode 100644 index 00000000000..d76ac911a18 Binary files /dev/null and b/.repos/alchemy-effect/website/public/favicon.png differ diff --git a/.repos/alchemy-effect/website/public/favicon.svg b/.repos/alchemy-effect/website/public/favicon.svg new file mode 100644 index 00000000000..d07e4c1b7dd --- /dev/null +++ b/.repos/alchemy-effect/website/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.repos/alchemy-effect/website/public/fonts/Tinos-Regular.ttf b/.repos/alchemy-effect/website/public/fonts/Tinos-Regular.ttf new file mode 100644 index 00000000000..893a1b52596 Binary files /dev/null and b/.repos/alchemy-effect/website/public/fonts/Tinos-Regular.ttf differ diff --git a/.repos/alchemy-effect/website/public/icon-512.png b/.repos/alchemy-effect/website/public/icon-512.png new file mode 100644 index 00000000000..db4c8b20793 Binary files /dev/null and b/.repos/alchemy-effect/website/public/icon-512.png differ diff --git a/.repos/alchemy-effect/website/public/llms.txt b/.repos/alchemy-effect/website/public/llms.txt new file mode 100644 index 00000000000..a30083a8ca6 --- /dev/null +++ b/.repos/alchemy-effect/website/public/llms.txt @@ -0,0 +1,302 @@ +# Alchemy + +> Alchemy Effect is an Infrastructure-as-Effects (IaE) framework that combines cloud infrastructure and application logic into a single, type-safe program powered by [Effect](https://effect.website). Resources are declared as Effects; bindings wire IAM, env vars, and typed SDKs in one call; deploys and runtime share the same code. + +This file is a navigation index for the documentation site at https://v2.alchemy.run. Every page under `/src/content/docs/` is listed below with its URL and a one-line summary, so an agent can pick the right page in one hop. + +## Start here + +- [What is Alchemy?](https://v2.alchemy.run/what-is-alchemy) — Alchemy is an Infrastructure-as-Effects framework that combines cloud infrastructure and application logic into a single type-safe program powered by Effect. +- [Getting Started](https://v2.alchemy.run/getting-started) — Install Alchemy and create your first Stack in under two minutes. + +## Tutorial — main path (Cloudflare) + +A linear five-part walkthrough from zero to a tested, locally-developed, CI-deployed Cloudflare project. Each part builds on the previous one. + +- [Part 1: Your First Stack](https://v2.alchemy.run/tutorial/part-1) — Install Alchemy, create a Stack with a Cloudflare R2 Bucket, and deploy it. +- [Part 2: Add a Worker](https://v2.alchemy.run/tutorial/part-2) — Create a Cloudflare Worker, bind the R2 Bucket, and implement GET/PUT routes. +- [Part 3: Testing](https://v2.alchemy.run/tutorial/part-3) — Write integration tests that deploy your stack and make HTTP requests against your live Worker. +- [Part 4: Local Dev](https://v2.alchemy.run/tutorial/part-4) — Run your stack locally with alchemy dev for hot reloading and instant feedback. +- [Part 5: CI/CD](https://v2.alchemy.run/tutorial/part-5) — Set up GitHub Actions for automated deployments, PR previews, and remote state — with Cloudflare credentials managed as code. + +## Tutorial — Cloudflare add-ons + +Standalone tutorials that extend the main tutorial's Worker with a specific Cloudflare feature. Pick the ones that match your use case. + +- [Add a Durable Object](https://v2.alchemy.run/tutorial/cloudflare/durable-objects) — Add a Durable Object to your Worker — persist state per key, expose RPC methods, and stream values back to the client. +- [Bind to another Worker's Durable Object](https://v2.alchemy.run/tutorial/cloudflare/cross-worker-durable-object) — Share a Durable Object across multiple Workers — one Worker hosts the runtime, others bind to it by scriptName for a typed RPC stub. +- [Accept WebSockets](https://v2.alchemy.run/tutorial/cloudflare/hibernatable-websockets) — Accept WebSocket connections in a Durable Object, broadcast between peers, and survive Cloudflare's hibernation. +- [Add a Vite SPA](https://v2.alchemy.run/tutorial/cloudflare/vite-spa) — Ship a Vite single-page app from the same Stack as your Worker — built, bundled, and deployed to Cloudflare in one command. +- [Run a Container](https://v2.alchemy.run/tutorial/cloudflare/containers) — Run a long-lived container alongside a Durable Object, expose RPC methods, and proxy HTTP requests to ports inside the container. +- [Add a Workflow](https://v2.alchemy.run/tutorial/cloudflare/workflows) — Orchestrate durable, multi-step work with Cloudflare Workflows — automatic retries, replayable steps, and at-least-once delivery. +- [Add an AI Gateway](https://v2.alchemy.run/tutorial/cloudflare/ai-gateway) — Wire an AI Gateway into your Worker, turn it into a typed Effect LanguageModel, and run generations and streams through Workers AI with caching, rate limiting, and logs. +- [Build a Git Repo API](https://v2.alchemy.run/tutorial/cloudflare/artifacts) — Build a mini-GitHub on Cloudflare — Artifacts for Git storage, a Durable Object per repo for metadata, fronted by a schema-typed HttpApi. +- [Connect to a Database with Hyperdrive](https://v2.alchemy.run/tutorial/cloudflare/hyperdrive) — Provision a Postgres or MySQL database (Neon, PlanetScale Postgres, or PlanetScale MySQL) and front it with Cloudflare Hyperdrive so your Worker reaches the database with edge-pooled, low-latency connections. +- [Add Drizzle ORM](https://v2.alchemy.run/tutorial/cloudflare/drizzle) — Replace raw pg with Drizzle's effect-postgres integration, manage your schema as a resource, and have alchemy generate and apply migrations on every deploy. +- [Branch from a shared database](https://v2.alchemy.run/tutorial/cloudflare/branch-from-shared-database) — Have ephemeral PR-preview stages reference a long-lived Neon project from the staging stage instead of provisioning their own — fast previews, copy-on-write branches, no extra Postgres clusters. +- [Consume from a Queue](https://v2.alchemy.run/tutorial/cloudflare/queue-consumer) — Receive messages from a Cloudflare Queue with Cloudflare.messages(queue).subscribe(...) — Effect-style stream handler with automatic ack/retry. +- [Define an RPC Worker](https://v2.alchemy.run/tutorial/cloudflare/rpc-worker) — Define a typed Effect RPC group, serve it from `Cloudflare.RpcWorker`, drive it from an integration test, and (later) bind a typed client from another Worker. +- [Add a typed RPC Durable Object](https://v2.alchemy.run/tutorial/cloudflare/rpc-durable-object) — Replace alchemy's built-in DO method bridge with `Cloudflare.RpcDurableObjectNamespace` — a typed RPC group served on the DO's `fetch`, with a class-instance-preserving wire format. Walk through a single DO, drive it from a test, then layer in a Worker. + +## Tutorial — AWS + +End-to-end AWS tutorials. Read the Lambda page first; the others bind storage and event sources to that Lambda. + +- [Deploy a Lambda Function](https://v2.alchemy.run/tutorial/aws/lambda) — Stand up an AWS Lambda Function from a single Effect, expose it over a Function URL, and call it from a test. +- [Read & Write S3](https://v2.alchemy.run/tutorial/aws/s3) — Add an S3 Bucket to your Stack, bind PutObject and GetObject as runtime capabilities, and let Alchemy mint the IAM policy for you. +- [React to S3 Events](https://v2.alchemy.run/tutorial/aws/s3-events) — Subscribe a Lambda Function to S3 bucket notifications, process them as an Effect Stream, and let Alchemy wire up the IAM and event-source plumbing. +- [Store Records in DynamoDB](https://v2.alchemy.run/tutorial/aws/dynamodb) — Add a DynamoDB Table, bind GetItem and PutItem to your Lambda, and serve a typed key/value HTTP API backed by DynamoDB. +- [Process DynamoDB Streams](https://v2.alchemy.run/tutorial/aws/dynamodb-streams) — Enable a DynamoDB Stream on your table and consume change records as a typed Effect Stream from the same Lambda. +- [Send & Consume SQS Messages](https://v2.alchemy.run/tutorial/aws/sqs) — Add an SQS Queue, publish messages from your Lambda, and consume them from a second consumer Lambda — all wired through Alchemy bindings. +- [Stream Records with Kinesis](https://v2.alchemy.run/tutorial/aws/kinesis) — Add a Kinesis Data Stream, publish records from one Lambda, and consume them in order from another — wired through the same Stream-shaped event source. +- [REST API (API Gateway v1)](https://v2.alchemy.run/tutorial/aws/api-gateway) — Expose a Lambda with a regional Amazon API Gateway REST API using RestApi, Resource, Method, Deployment, and Stage primitives. + +## Concepts — the mental model + +Reference pages explaining what each primitive means and how they fit together. Read these when something in a tutorial feels magical, or before designing a new Stack. + +- [Stack](https://v2.alchemy.run/concepts/stack) — A Stack is a collection of Resources deployed together as a unit. +- [Resource](https://v2.alchemy.run/concepts/resource) — Resources are named cloud entities with input properties and output attributes. +- [Action](https://v2.alchemy.run/concepts/action) — A node in the dependency graph that runs an Effect during apply when its inputs change. +- [Inputs and Outputs](https://v2.alchemy.run/concepts/outputs) — Output is alchemy's lazy reference type — the lazy values that flow between resources, get composed with .pipe, mapped, interpolated, and resolved during deploy. +- [References](https://v2.alchemy.run/concepts/references) — Read values out of another stack or stage at plan time — typed, lazy, and resolved from persisted state. +- [Resource Lifecycle](https://v2.alchemy.run/concepts/resource-lifecycle) — How alchemy plans, applies, replaces, and destroys resources — and how to think about idempotency and recovery. +- [Provider](https://v2.alchemy.run/concepts/provider) — Providers implement the lifecycle operations for a resource type — reconcile, delete, diff, read, and more. +- [Platform](https://v2.alchemy.run/concepts/platform) — A Platform bundles infrastructure with the runtime code that runs on it — Workers, Lambda Functions, Containers — so your handler ships with its bindings. +- [Phases](https://v2.alchemy.run/concepts/phases) — Alchemy programs run in two phases — plantime/init drives the deploy, runtime handles requests. Knowing which is which is the key to using Platforms. +- [Binding](https://v2.alchemy.run/concepts/binding) — A binding connects a resource to a Worker or Lambda. It generates IAM policies, env vars, and a typed SDK in one call. +- [Secrets and Config](https://v2.alchemy.run/concepts/secrets) — Use effect/Config to read env vars at init time and have Alchemy automatically bind them onto the deploy target. +- [Layers](https://v2.alchemy.run/concepts/layers) — A Layer encapsulates a slice of infrastructure (resources, bindings, runtime logic) behind a typed service interface. Code that depends on the interface stays cloud-agnostic; swapping the implementation swaps the underlying infrastructure. +- [State Store](https://v2.alchemy.run/concepts/state-store) — How Alchemy persists resource state between deploys to compute diffs and track infrastructure. +- [Testing](https://v2.alchemy.run/concepts/testing) — Reference for alchemy/Test — every helper, hook, and option exposed by Test.make for Bun and Vitest. +- [Local Development](https://v2.alchemy.run/concepts/local-development) — How alchemy dev provides hot reloading, local workerd execution, and cloud-backed resources for fast iteration. +- [Observability](https://v2.alchemy.run/concepts/observability) — Effect emits OpenTelemetry — ship traces, metrics, and logs to Axiom, Datadog, CloudWatch, or any OTLP endpoint. Declare dashboards and alarms in code. +- [Profiles](https://v2.alchemy.run/concepts/profiles) — Profiles store cloud credentials per environment in ~/.alchemy/profiles.json — switch between work and personal accounts, or between staging and prod credentials. +- [Stages](https://v2.alchemy.run/concepts/stages) — Stages are isolated instances of a Stack — dev_sam, staging, prod, pr-42 — each with their own state and physical names. + +## Guides — task-oriented + +Standalone how-to pages. Each solves a specific problem; read in any order. + +- [Migrating from v1](https://v2.alchemy.run/guides/migrating-from-v1) — Migrate your Alchemy v1 (async/await) project to Alchemy v2. +- [CLI Reference](https://v2.alchemy.run/guides/cli) — All Alchemy CLI commands — deploy, destroy, plan, dev, tail, logs, aws, cloudflare, login, profile, and state. +- [Continuous Integration](https://v2.alchemy.run/guides/ci) — Set up CI/CD pipelines for alchemy projects with GitHub Actions, automated deployments, and PR previews — with provider credentials managed as code. +- [Monorepos](https://v2.alchemy.run/guides/monorepo) — Two patterns for organizing an Alchemy monorepo with a backend API and a frontend website — one shared stack (recommended) or one stack per package — with the trade-offs and a working example for each. +- [Secrets and env vars](https://v2.alchemy.run/guides/secrets) — Wire OPENAI_API_KEY from .env into a Cloudflare Worker as a secret_text binding. +- [Effect HTTP API](https://v2.alchemy.run/guides/effect-http-api) — Build a schema-validated HTTP API with Effect's HttpApi module and deploy it as a Cloudflare Worker. +- [Shared database across stages](https://v2.alchemy.run/guides/shared-database) — Have ephemeral PR-preview stages reference a long-lived Neon Postgres project from staging instead of provisioning their own — fast previews, copy-on-write branches, no extra Postgres clusters. +- [Effect RPC](https://v2.alchemy.run/guides/effect-rpc) — Build a typed RPC API with Effect's Rpc module and deploy it as a Cloudflare Worker. +- [Frontend frameworks](https://v2.alchemy.run/guides/frontends) — Deploy Vite-based frameworks (TanStack Start, Astro, SolidStart, Nuxt, React) and any custom-built static site (Hugo, Eleventy) to Cloudflare with one declaration. +- [Circular Bindings](https://v2.alchemy.run/guides/circular-bindings) — How to model two services that reference each other (Worker A ↔ Worker B, Lambda ↔ Lambda) using tagged classes and Layers. +- [Effect AI](https://v2.alchemy.run/guides/effect-ai) — Wire Effect's LanguageModel and Chat services into a Cloudflare Worker — read API keys with effect/Config, provide the model layer to your handler, plug in persistence. +- [Building Infrastructure Layers](https://v2.alchemy.run/guides/infrastructure-layers) — Package resources + bindings + runtime glue into a typed Effect Layer, then swap a KV-backed implementation for an R2-backed one without touching the consumer. +- [Writing a Custom State Store](https://v2.alchemy.run/guides/custom-state-store) — Build a Postgres-backed Alchemy state store step by step — implement the StateService interface, plug it into a stack, and test it end-to-end. +- [Writing a Custom Resource Provider](https://v2.alchemy.run/guides/custom-provider) — Add support for a new cloud or third-party API by declaring a Resource type and implementing its lifecycle as an Effect Layer. + +## Providers + +Per-resource API reference, generated from JSDoc on the source `.ts` files via `bun generate:api-reference`. Each page documents the resource's input properties (with types, defaults, and constraints), output attributes, and Quick Reference / Examples sections derived from `@section` / `@example` JSDoc tags. Grouped by cloud below. + +### AWS + +- [AccessEntry](https://v2.alchemy.run/providers/aws/eks/accessentry) — An Amazon EKS access entry that grants an IAM principal access to a cluster. +- [AccessKey](https://v2.alchemy.run/providers/aws/iam/accesskey) — An IAM access key for a user. +- [Account](https://v2.alchemy.run/providers/aws/apigateway/account) — Account-level settings for Amazon API Gateway in the current region (CloudWatch logging role, etc.). +- [Account](https://v2.alchemy.run/providers/aws/organizations/account) — A member account created and managed by AWS Organizations. +- [AccountAlias](https://v2.alchemy.run/providers/aws/iam/accountalias) — The singleton IAM account alias for an AWS account. +- [AccountAssignment](https://v2.alchemy.run/providers/aws/identitycenter/accountassignment) — Assigns an IAM Identity Center permission set to a user or group in an AWS account. +- [AccountPasswordPolicy](https://v2.alchemy.run/providers/aws/iam/accountpasswordpolicy) — The singleton IAM account password policy. +- [Addon](https://v2.alchemy.run/providers/aws/eks/addon) — An Amazon EKS managed add-on installed on a cluster. +- [Alarm](https://v2.alchemy.run/providers/aws/cloudwatch/alarm) — A CloudWatch metric alarm. +- [AlarmMuteRule](https://v2.alchemy.run/providers/aws/cloudwatch/alarmmuterule) — A CloudWatch alarm mute rule. +- [AnomalyDetector](https://v2.alchemy.run/providers/aws/cloudwatch/anomalydetector) — A CloudWatch anomaly detector. +- [ApiKey](https://v2.alchemy.run/providers/aws/apigateway/apikey) — API Gateway API key for usage plans and `apiKeyRequired` methods. +- [AssetDeployment](https://v2.alchemy.run/providers/aws/website/assetdeployment) — Upload a local directory into S3 with website-friendly defaults. +- [Authorizer](https://v2.alchemy.run/providers/aws/apigateway/authorizer) — REST API Lambda, Cognito, or gateway authorizer. +- [AutoScalingGroup](https://v2.alchemy.run/providers/aws/autoscaling/autoscalinggroup) — An EC2 Auto Scaling Group that manages a fleet of instances from a launch template and can register that fleet with one or more load balancer target groups. +- [BasePathMapping](https://v2.alchemy.run/providers/aws/apigateway/basepathmapping) — Maps a custom domain name path to a REST API stage. +- [Bucket](https://v2.alchemy.run/providers/aws/s3/bucket) — An S3 bucket for storing objects in AWS. +- [CachePolicy](https://v2.alchemy.run/providers/aws/cloudfront/cachepolicy) — A CloudFront cache policy. +- [CapacityProvider](https://v2.alchemy.run/providers/aws/ecs/capacityprovider) — An Amazon ECS capacity provider backed by an EC2 Auto Scaling Group. +- [Certificate](https://v2.alchemy.run/providers/aws/acm/certificate) — An ACM certificate for CloudFront and other AWS endpoints. +- [Cluster](https://v2.alchemy.run/providers/aws/ecs/cluster) — An Amazon ECS cluster for running tasks and services. +- [Cluster](https://v2.alchemy.run/providers/aws/eks/cluster) — An Amazon EKS cluster with support for EKS Auto Mode settings. +- [CompositeAlarm](https://v2.alchemy.run/providers/aws/cloudwatch/compositealarm) — A CloudWatch composite alarm. +- [Dashboard](https://v2.alchemy.run/providers/aws/cloudwatch/dashboard) — An Amazon CloudWatch dashboard. +- [DBCluster](https://v2.alchemy.run/providers/aws/rds/dbcluster) — An Aurora DB cluster. +- [DBClusterEndpoint](https://v2.alchemy.run/providers/aws/rds/dbclusterendpoint) — A custom Aurora cluster endpoint. +- [DBClusterParameterGroup](https://v2.alchemy.run/providers/aws/rds/dbclusterparametergroup) — An Aurora cluster parameter group. +- [DBInstance](https://v2.alchemy.run/providers/aws/rds/dbinstance) — An Aurora cluster instance. +- [DBParameterGroup](https://v2.alchemy.run/providers/aws/rds/dbparametergroup) — An RDS DB parameter group, useful for Aurora cluster instances. +- [DBProxy](https://v2.alchemy.run/providers/aws/rds/dbproxy) — An RDS Proxy for pooled Lambda-to-Aurora connectivity. +- [DBProxyEndpoint](https://v2.alchemy.run/providers/aws/rds/dbproxyendpoint) — An additional RDS Proxy endpoint. +- [DBProxyTargetGroup](https://v2.alchemy.run/providers/aws/rds/dbproxytargetgroup) — The proxy target group that registers Aurora clusters or instances behind an RDS Proxy. +- [DBSubnetGroup](https://v2.alchemy.run/providers/aws/rds/dbsubnetgroup) — An RDS DB subnet group for Aurora clusters, instances, and proxies. +- [DelegatedAdministrator](https://v2.alchemy.run/providers/aws/organizations/delegatedadministrator) — Registers a delegated administrator account for a trusted AWS service. +- [Deployment](https://v2.alchemy.run/providers/aws/apigateway/deployment) — A point-in-time snapshot of a REST API, ready to be served through a `Stage`. +- [Distribution](https://v2.alchemy.run/providers/aws/cloudfront/distribution) — A CloudFront distribution. +- [DomainName](https://v2.alchemy.run/providers/aws/apigateway/domainname) — Custom domain name for an Amazon API Gateway REST API. +- [EgressOnlyInternetGateway](https://v2.alchemy.run/providers/aws/ec2/egressonlyinternetgateway) — API reference for EgressOnlyInternetGateway +- [EIP](https://v2.alchemy.run/providers/aws/ec2/eip) — API reference for EIP +- [EventBus](https://v2.alchemy.run/providers/aws/eventbridge/eventbus) — An Amazon EventBridge event bus for receiving and routing events. +- [EventSourceMapping](https://v2.alchemy.run/providers/aws/lambda/eventsourcemapping) — API reference for EventSourceMapping +- [Function](https://v2.alchemy.run/providers/aws/cloudfront/function) — A CloudFront Function for viewer request and response customization. +- [Function](https://v2.alchemy.run/providers/aws/lambda/function) — An AWS Lambda host resource that combines code bundling, IAM role provisioning, and runtime binding collection. +- [GatewayResponse](https://v2.alchemy.run/providers/aws/apigateway/gatewayresponse) — Gateway response mapping for a REST API (e.g. DEFAULT_4XX, DEFAULT_5XX). +- [Group](https://v2.alchemy.run/providers/aws/iam/group) — An IAM group that can own managed and inline policies. +- [Group](https://v2.alchemy.run/providers/aws/identitycenter/group) — A group in the IAM Identity Center identity store. +- [GroupMembership](https://v2.alchemy.run/providers/aws/iam/groupmembership) — An explicit IAM group membership resource that owns a group's managed users. +- [InsightRule](https://v2.alchemy.run/providers/aws/cloudwatch/insightrule) — A CloudWatch Contributor Insights rule. +- [Instance](https://v2.alchemy.run/providers/aws/ec2/instance) — An EC2 instance that can either act as a low-level compute primitive or run a bundled long-lived Effect program directly on the machine. +- [Instance](https://v2.alchemy.run/providers/aws/identitycenter/instance) — An IAM Identity Center instance visible to the current account. +- [InstanceProfile](https://v2.alchemy.run/providers/aws/iam/instanceprofile) — An IAM instance profile that can present a role to EC2 instances. +- [InternetGateway](https://v2.alchemy.run/providers/aws/ec2/internetgateway) — API reference for InternetGateway +- [Invalidation](https://v2.alchemy.run/providers/aws/cloudfront/invalidation) — A CloudFront cache invalidation request. +- [KeyGroup](https://v2.alchemy.run/providers/aws/cloudfront/keygroup) — A CloudFront key group. +- [KeyValueStore](https://v2.alchemy.run/providers/aws/cloudfront/keyvaluestore) — A CloudFront KeyValueStore for edge metadata. +- [KvEntries](https://v2.alchemy.run/providers/aws/cloudfront/kventries) — Manages namespaced key-value entries in a CloudFront KeyValueStore. +- [KvRoutesUpdate](https://v2.alchemy.run/providers/aws/cloudfront/kvroutesupdate) — Manages a single route entry in a JSON array stored in a CloudFront KeyValueStore. +- [LaunchTemplate](https://v2.alchemy.run/providers/aws/autoscaling/launchtemplate) — A launch template that preserves the `Host` authoring model used by `AWS.EC2.Instance`, but packages that host configuration for use with an Auto Scaling Group. +- [Listener](https://v2.alchemy.run/providers/aws/elbv2/listener) — API reference for Listener +- [LoadBalancer](https://v2.alchemy.run/providers/aws/elbv2/loadbalancer) — API reference for LoadBalancer +- [LogGroup](https://v2.alchemy.run/providers/aws/logs/loggroup) — A CloudWatch Logs log group. +- [LoginProfile](https://v2.alchemy.run/providers/aws/iam/loginprofile) — An IAM console login profile for a user. +- [Method](https://v2.alchemy.run/providers/aws/apigateway/method) — An HTTP method on an API Gateway Resource. +- [MetricStream](https://v2.alchemy.run/providers/aws/cloudwatch/metricstream) — A CloudWatch metric stream. +- [NatGateway](https://v2.alchemy.run/providers/aws/ec2/natgateway) — API reference for NatGateway +- [NetworkAcl](https://v2.alchemy.run/providers/aws/ec2/networkacl) — API reference for NetworkAcl +- [NetworkAclAssociation](https://v2.alchemy.run/providers/aws/ec2/networkaclassociation) — API reference for NetworkAclAssociation +- [NetworkAclEntry](https://v2.alchemy.run/providers/aws/ec2/networkaclentry) — API reference for NetworkAclEntry +- [OpenIDConnectProvider](https://v2.alchemy.run/providers/aws/iam/openidconnectprovider) — An IAM OpenID Connect provider for web identity federation. +- [Organization](https://v2.alchemy.run/providers/aws/organizations/organization) — The AWS Organization for the current management account. +- [OrganizationalUnit](https://v2.alchemy.run/providers/aws/organizations/organizationalunit) — An AWS Organizations organizational unit. +- [OrganizationResourcePolicy](https://v2.alchemy.run/providers/aws/organizations/organizationresourcepolicy) — The singleton AWS Organizations resource policy. +- [OriginAccessControl](https://v2.alchemy.run/providers/aws/cloudfront/originaccesscontrol) — A CloudFront Origin Access Control for private origins. +- [OriginRequestPolicy](https://v2.alchemy.run/providers/aws/cloudfront/originrequestpolicy) — A CloudFront origin request policy. +- [Permission](https://v2.alchemy.run/providers/aws/eventbridge/permission) — An EventBridge event bus permission statement. +- [Permission](https://v2.alchemy.run/providers/aws/lambda/permission) — A Lambda permission that grants an AWS service or another account permission to invoke a function. +- [PermissionSet](https://v2.alchemy.run/providers/aws/identitycenter/permissionset) — An IAM Identity Center permission set. +- [PodIdentityAssociation](https://v2.alchemy.run/providers/aws/eks/podidentityassociation) — An Amazon EKS pod identity association that binds a service account to an IAM role. +- [Policy](https://v2.alchemy.run/providers/aws/iam/policy) — A customer-managed IAM policy. +- [Policy](https://v2.alchemy.run/providers/aws/organizations/policy) — An AWS Organizations policy such as an SCP or tag policy. +- [PolicyAttachment](https://v2.alchemy.run/providers/aws/organizations/policyattachment) — Attaches an Organizations policy to a root, OU, or account. +- [PublicKey](https://v2.alchemy.run/providers/aws/cloudfront/publickey) — A CloudFront public key. +- [Queue](https://v2.alchemy.run/providers/aws/sqs/queue) — An Amazon SQS queue for reliable, decoupled message processing. +- [Record](https://v2.alchemy.run/providers/aws/route53/record) — A Route 53 DNS record set. +- [Repository](https://v2.alchemy.run/providers/aws/ecr/repository) — An Amazon ECR repository for container images. +- [ResponseHeadersPolicy](https://v2.alchemy.run/providers/aws/cloudfront/responseheaderspolicy) — A CloudFront response headers policy. +- [RestApi](https://v2.alchemy.run/providers/aws/apigateway/restapi) — An Amazon API Gateway REST API (v1). +- [Role](https://v2.alchemy.run/providers/aws/iam/role) — An IAM role for AWS services and runtimes. +- [Root](https://v2.alchemy.run/providers/aws/organizations/root) — The organization root. +- [RootPolicyType](https://v2.alchemy.run/providers/aws/organizations/rootpolicytype) — Enables a policy type on an organization root. +- [Route](https://v2.alchemy.run/providers/aws/ec2/route) — API reference for Route +- [RouteTable](https://v2.alchemy.run/providers/aws/ec2/routetable) — API reference for RouteTable +- [RouteTableAssociation](https://v2.alchemy.run/providers/aws/ec2/routetableassociation) — API reference for RouteTableAssociation +- [Rule](https://v2.alchemy.run/providers/aws/eventbridge/rule) — An Amazon EventBridge rule that matches events and routes them to targets. +- [SAMLProvider](https://v2.alchemy.run/providers/aws/iam/samlprovider) — An IAM SAML identity provider. +- [ScalingPolicy](https://v2.alchemy.run/providers/aws/autoscaling/scalingpolicy) — A target-tracking scaling policy for an Auto Scaling Group. +- [Schedule](https://v2.alchemy.run/providers/aws/scheduler/schedule) — An EventBridge Scheduler schedule. +- [ScheduleGroup](https://v2.alchemy.run/providers/aws/scheduler/schedulegroup) — An EventBridge Scheduler schedule group. +- [Secret](https://v2.alchemy.run/providers/aws/secretsmanager/secret) — An AWS Secrets Manager secret. +- [SecurityGroup](https://v2.alchemy.run/providers/aws/ec2/securitygroup) — Ingress or egress rule for a security group. +- [SecurityGroupRule](https://v2.alchemy.run/providers/aws/ec2/securitygrouprule) — API reference for SecurityGroupRule +- [ServerCertificate](https://v2.alchemy.run/providers/aws/iam/servercertificate) — An IAM server certificate. +- [Service](https://v2.alchemy.run/providers/aws/ecs/service) — An ECS Fargate service for running long-lived tasks. +- [ServiceSpecificCredential](https://v2.alchemy.run/providers/aws/iam/servicespecificcredential) — A service-specific IAM credential. +- [SigningCertificate](https://v2.alchemy.run/providers/aws/iam/signingcertificate) — An IAM signing certificate for a user. +- [SSHPublicKey](https://v2.alchemy.run/providers/aws/iam/sshpublickey) — An IAM SSH public key for CodeCommit-compatible workflows. +- [Stage](https://v2.alchemy.run/providers/aws/apigateway/stage) — A stage for a REST API deployment. +- [Stream](https://v2.alchemy.run/providers/aws/kinesis/stream) — An Amazon Kinesis Data Stream. +- [StreamConsumer](https://v2.alchemy.run/providers/aws/kinesis/streamconsumer) — A registered Kinesis enhanced fan-out consumer. +- [Subnet](https://v2.alchemy.run/providers/aws/ec2/subnet) — API reference for Subnet +- [Subscription](https://v2.alchemy.run/providers/aws/sns/subscription) — An Amazon SNS subscription that attaches an endpoint to a topic. +- [Table](https://v2.alchemy.run/providers/aws/dynamodb/table) — An Amazon DynamoDB table with optional indexes, PITR, TTL, and stream-aware binding support. +- [TargetGroup](https://v2.alchemy.run/providers/aws/elbv2/targetgroup) — API reference for TargetGroup +- [Task](https://v2.alchemy.run/providers/aws/ecs/task) — API reference for Task +- [Topic](https://v2.alchemy.run/providers/aws/sns/topic) — An Amazon SNS topic for fan-out messaging and notifications. +- [TrustedServiceAccess](https://v2.alchemy.run/providers/aws/organizations/trustedserviceaccess) — Enables trusted access for an AWS service principal. +- [UsagePlan](https://v2.alchemy.run/providers/aws/apigateway/usageplan) — Usage plan for API stages, throttling, and quotas. +- [UsagePlanKey](https://v2.alchemy.run/providers/aws/apigateway/usageplankey) — Associates an API key with a usage plan. +- [User](https://v2.alchemy.run/providers/aws/iam/user) — An IAM user with optional inline policies, managed policies, and tags. +- [VirtualMFADevice](https://v2.alchemy.run/providers/aws/iam/virtualmfadevice) — An IAM virtual MFA device. +- [Vpc](https://v2.alchemy.run/providers/aws/ec2/vpc) — API reference for Vpc +- [VpcEndpoint](https://v2.alchemy.run/providers/aws/ec2/vpcendpoint) — API reference for VpcEndpoint +- [VpcLink](https://v2.alchemy.run/providers/aws/apigateway/vpclink) — VPC link for private integrations (`connectionType: \"VPC_LINK\"` on a method integration). + +### Axiom + +- [Annotation](https://v2.alchemy.run/providers/axiom/annotation) — An Axiom annotation — a vertical marker overlaid on charts to flag a deploy, incident, feature flag flip, or any other point/range event you want correlated with telemetry. +- [ApiToken](https://v2.alchemy.run/providers/axiom/apitoken) — An Axiom API token — a scoped bearer token used to authenticate API requests (ingest, query, admin). Capabilities are pinned at creation time; changing any field triggers a **replacement** because Axiom does not expose an update endpoint. +- [Dashboard](https://v2.alchemy.run/providers/axiom/dashboard) — An Axiom dashboard — a named, layout-driven collection of charts. Each dashboard takes a full document (`charts` + `layout` array of grid cells + `timeWindow` + `refreshTime`) at version `schemaVersion: 2`. +- [Dataset](https://v2.alchemy.run/providers/axiom/dataset) — An Axiom dataset — the top-level container that stores events, logs, traces, or metrics. Pick a `kind` up-front: it determines schema and how the data is shown in the UI, and **cannot be changed** after creation (changing it triggers a replacement, which deletes the data). +- [Monitor](https://v2.alchemy.run/providers/axiom/monitor) — An Axiom monitor — a scheduled APL/MPL query that evaluates on a fixed cadence and fires alerts via {@link Notifier notifiers} when its condition is met. +- [Notifier](https://v2.alchemy.run/providers/axiom/notifier) — An Axiom notifier — an alert destination (Slack, email, PagerDuty, Opsgenie, Discord, Microsoft Teams, generic webhook, or a fully custom webhook with templated body/headers) that {@link Monitor monitors} target via `notifierIds`. Exactly one channel under `properties` should be set. +- [View](https://v2.alchemy.run/providers/axiom/view) — An Axiom saved view — a named, shareable APL query. Useful for building starter dashboards, providing canned \"open in Axiom\" links from your app, or pinning common investigations the team revisits. +- [VirtualField](https://v2.alchemy.run/providers/axiom/virtualfield) — An Axiom virtual field — a saved APL expression that appears as a derived column on a dataset at query time. Use these to standardise common computations (status classes, latency buckets, parsed JSON paths) so dashboards and monitors don't have to redefine them. + +### Build + +- [Command](https://v2.alchemy.run/providers/build/command) — A Build resource that runs a shell command and produces an output asset. Input files are hashed using globs to avoid redundant rebuilds. + +### Cloudflare + +- [AccountApiToken](https://v2.alchemy.run/providers/cloudflare/accountapitoken) — A Cloudflare account-owned API token (`POST /accounts/{account_id}/tokens`). +- [AiGateway](https://v2.alchemy.run/providers/cloudflare/aigateway) — A Cloudflare AI Gateway for observability, caching, rate limiting, and governance across AI provider requests. +- [AiGatewayBinding](https://v2.alchemy.run/providers/cloudflare/aigatewaybinding) — Binding service that turns an {@link AiGatewayResource} resource into a typed {@link AiGatewayClient} for Worker runtime code. Wraps the Cloudflare AI Gateway runtime binding so each operation returns an Effect tagged with {@link AiGatewayError}, exposes the raw Workers AI handle for `ai.run(...)`, and provides a `model(options)` factory that produces an `effect/unstable/ai` `LanguageModel` `Layer`. +- [AnalyticsEngineDataset](https://v2.alchemy.run/providers/cloudflare/analyticsenginedataset) — A Cloudflare Workers Analytics Engine dataset binding. +- [BrowserRendering](https://v2.alchemy.run/providers/cloudflare/browserrendering) — A Cloudflare Browser Rendering binding for launching headless browser sessions from Workers via `@cloudflare/puppeteer`. +- [Container](https://v2.alchemy.run/providers/cloudflare/container) — A Cloudflare Container that runs a long-lived process alongside a Durable Object. +- [D1Database](https://v2.alchemy.run/providers/cloudflare/d1database) — A Cloudflare D1 serverless SQL database built on SQLite. +- [DurableObjectNamespace](https://v2.alchemy.run/providers/cloudflare/durableobjectnamespace) — A Cloudflare Durable Object namespace that manages globally unique, stateful instances with WebSocket hibernation support. +- [DynamicWorkerLoader](https://v2.alchemy.run/providers/cloudflare/dynamicworkerloader) — Load and run ephemeral Workers at runtime from inline JavaScript modules. +- [EmailAddress](https://v2.alchemy.run/providers/cloudflare/emailaddress) — A verified destination email address on the account. +- [EmailRouting](https://v2.alchemy.run/providers/cloudflare/emailrouting) — Enables Cloudflare Email Routing on a zone. This is the prerequisite for receiving mail at any address on the domain and for sending email from a Worker via `send_email` bindings. +- [EmailRule](https://v2.alchemy.run/providers/cloudflare/emailrule) — A Cloudflare Email Routing rule. +- [Hyperdrive](https://v2.alchemy.run/providers/cloudflare/hyperdrive) — A Cloudflare Hyperdrive configuration. +- [HyperdriveBinding](https://v2.alchemy.run/providers/cloudflare/hyperdrivebinding) — A typed accessor for a Cloudflare Hyperdrive runtime binding inside a Worker. Provides the same shape as the raw `Hyperdrive` runtime object (connection string, host, port, user, password, database) plus a `raw` escape hatch for libraries that want direct access. +- [KVNamespace](https://v2.alchemy.run/providers/cloudflare/kvnamespace) — A Cloudflare Workers KV namespace for key-value storage at the edge. +- [Queue](https://v2.alchemy.run/providers/cloudflare/queue) — A Cloudflare Queue for reliable message passing between Workers. +- [QueueConsumer](https://v2.alchemy.run/providers/cloudflare/queueconsumer) — A Cloudflare Queue Consumer that processes messages from a Queue. +- [R2Bucket](https://v2.alchemy.run/providers/cloudflare/r2bucket) — A Cloudflare R2 object storage bucket with S3-compatible API. +- [RpcDurableObjectNamespace](https://v2.alchemy.run/providers/cloudflare/rpcdurableobjectnamespace) — `RpcDurableObjectNamespace` is sugar over {@link DurableObjectNamespace} for Durable Objects whose surface is a typed Effect `RpcGroup`. The DO serves an `RpcServer.toHttpEffect(group)` on its own `fetch`, and consumers see `namespace.getByName(id)` as a typed `RpcClient` directly — no manual client wiring. +- [RpcWorker](https://v2.alchemy.run/providers/cloudflare/rpcworker) — `RpcWorker` is a thin sugar over {@link Worker} for the common case where a worker's entire `fetch` surface is a typed Effect `RpcGroup`. It takes the rpc `schema` directly in props alongside `main`, and accepts an init Effect that returns the already-piped `RpcServer.toHttpEffect(...)`-producing Effect (no `{ fetch }` wrapper) — the wrapper plugs it into the worker's `fetch` for you. +- [Secret](https://v2.alchemy.run/providers/cloudflare/secret) — A single secret stored inside a Cloudflare Secrets Store. +- [SecretsStore](https://v2.alchemy.run/providers/cloudflare/secretsstore) — A Cloudflare Secrets Store, a per-account container for secrets that can be bound into Workers with full redaction and audit support. +- [SendEmail](https://v2.alchemy.run/providers/cloudflare/sendemail) — A Cloudflare Workers `send_email` binding descriptor. +- [SendEmailBinding](https://v2.alchemy.run/providers/cloudflare/sendemailbinding) — A typed runtime accessor for a Cloudflare `send_email` Worker binding. +- [StaticSite](https://v2.alchemy.run/providers/cloudflare/staticsite) — A Cloudflare Worker that serves static assets built by a shell command. +- [Tunnel](https://v2.alchemy.run/providers/cloudflare/tunnel) — A Cloudflare Tunnel that establishes a secure connection from your origin to Cloudflare's edge. +- [UserApiToken](https://v2.alchemy.run/providers/cloudflare/userapitoken) — A Cloudflare user-owned API token (`POST /user/tokens`). +- [VectorizeIndex](https://v2.alchemy.run/providers/cloudflare/vectorizeindex) — A Cloudflare Vectorize index for storing and querying vector embeddings. +- [VectorizeMetadataIndex](https://v2.alchemy.run/providers/cloudflare/vectorizemetadataindex) — A metadata index on a Cloudflare Vectorize index. +- [Vite](https://v2.alchemy.run/providers/cloudflare/vite) — A Cloudflare Worker deployed from a Vite project. +- [VpcService](https://v2.alchemy.run/providers/cloudflare/vpcservice) — A Cloudflare VPC service that exposes a private host (IP or hostname) reachable through a Cloudflare Tunnel for Workers VPC. +- [Worker](https://v2.alchemy.run/providers/cloudflare/worker) — A Cloudflare Worker host with deploy-time binding support and runtime export collection. +- [Workflow](https://v2.alchemy.run/providers/cloudflare/workflow) — Service that carries the current workflow event payload. `yield* WorkflowEvent` inside a workflow body to access it. +- [ZarazConfig](https://v2.alchemy.run/providers/cloudflare/zarazconfig) — A Cloudflare Zaraz zone configuration. + +### Drizzle + +- [Postgres](https://v2.alchemy.run/providers/drizzle/postgres) — Open a Drizzle/Postgres database from a connection URL using the `drizzle-orm/effect-postgres` integration. +- [Schema](https://v2.alchemy.run/providers/drizzle/schema) — A Drizzle schema managed as an Alchemy resource. + +### GitHub + +- [Comment](https://v2.alchemy.run/providers/github/comment) — A GitHub issue or pull request comment. +- [Secret](https://v2.alchemy.run/providers/github/secret) — A GitHub Actions repository or environment secret. +- [Variable](https://v2.alchemy.run/providers/github/variable) — A GitHub Actions repository variable. + +### Neon + +- [Branch](https://v2.alchemy.run/providers/neon/branch) — A branch of a Neon project. +- [Project](https://v2.alchemy.run/providers/neon/project) — A Neon serverless Postgres project. + +### Planetscale + +- [MySQLBranch](https://v2.alchemy.run/providers/planetscale/mysql/mysqlbranch) — A PlanetScale branch of a {@link MySQLDatabase}. For PostgreSQL branches use {@link PostgresBranch} instead. +- [MySQLDatabase](https://v2.alchemy.run/providers/planetscale/mysql/mysqldatabase) — A MySQL PlanetScale database (powered by Vitess). For PostgreSQL use {@link PostgresDatabase} instead. +- [MySQLPassword](https://v2.alchemy.run/providers/planetscale/mysql/mysqlpassword) — A PlanetScale password for accessing a MySQL database branch. +- [PostgresBranch](https://v2.alchemy.run/providers/planetscale/postgres/postgresbranch) — A PlanetScale branch of a {@link PostgresDatabase}. For MySQL branches use {@link MySQLBranch} instead. +- [PostgresDatabase](https://v2.alchemy.run/providers/planetscale/postgres/postgresdatabase) — A PostgreSQL PlanetScale database. For MySQL, use {@link MySQLDatabase} instead. +- [PostgresDefaultRole](https://v2.alchemy.run/providers/planetscale/postgres/postgresdefaultrole) — The default PlanetScale PostgreSQL role for a database branch. +- [PostgresRole](https://v2.alchemy.run/providers/planetscale/postgres/postgresrole) — A PlanetScale role for accessing a PostgreSQL database branch. diff --git a/.repos/alchemy-effect/website/public/og-default.png b/.repos/alchemy-effect/website/public/og-default.png new file mode 100644 index 00000000000..e65092aa24c Binary files /dev/null and b/.repos/alchemy-effect/website/public/og-default.png differ diff --git a/.repos/alchemy-effect/website/public/og-default.svg b/.repos/alchemy-effect/website/public/og-default.svg new file mode 100644 index 00000000000..19e793dba5f --- /dev/null +++ b/.repos/alchemy-effect/website/public/og-default.svg @@ -0,0 +1,23 @@ + + + + + + + + + + alchemy + + ZERO → PRODUCTION + + + alchemy.run + \ No newline at end of file diff --git a/.repos/alchemy-effect/website/scripts/download-fonts.ts b/.repos/alchemy-effect/website/scripts/download-fonts.ts new file mode 100644 index 00000000000..6bcbaa8fbf2 --- /dev/null +++ b/.repos/alchemy-effect/website/scripts/download-fonts.ts @@ -0,0 +1,212 @@ +/** + * Downloads the static (non-variable) TTFs for our brand fonts. Two + * destinations: + * + * - `website/assets/fonts/` — build-time only, read by the OG image + * renderer (satori) via `fs.readFile`. Never shipped to clients. + * - `website/public/fonts/` — served by Astro at `/fonts/` for + * the website's own `@font-face` declarations. Use this only for + * fonts the runtime page actually needs. + * + * Why static, not variable: satori's opentype parser + * (`@shuding/opentype.js`) can't parse Google Fonts' variable TTFs (the + * ones with `[opsz,wght]` axes). Static TTFs work fine and the upstream + * static releases include the full glyph set unlike the `@fontsource/*` + * woff packages, which are subsetted to `latin` only and miss arrows. + * + * Files are cached on disk; subsequent runs are no-ops unless missing. + */ + +import { mkdir, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const buildOnlyFontsDir = path.resolve(here, "../assets/fonts"); +const publicFontsDir = path.resolve(here, "../public/fonts"); + +interface FontSource { + file: string; + url: string; + /** + * Where the file lands on disk. `"build"` (default) goes to + * `assets/fonts/` for OG-only consumption; `"public"` goes to + * `public/fonts/` so the website's CSS can fetch it at runtime. + */ + scope?: "build" | "public"; +} + +const FONTS: FontSource[] = [ + // Source Serif 4 — Adobe's official static TTFs. + // + // Two optical-size variants: Display (chunkier serifs, more stroke + // contrast) for the headline at ~100px, and Text (calmer, more even + // weight) for the description at ~26px. The website's hero uses the + // variable font which auto-selects optical size; satori needs us to + // pick explicitly per element. + { + file: "SourceSerif4-Regular.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4-Regular.ttf", + }, + { + file: "SourceSerif4-It.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4-It.ttf", + }, + { + file: "SourceSerif4Display-Regular.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4Display-Regular.ttf", + }, + { + file: "SourceSerif4Display-It.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4Display-It.ttf", + }, + { + file: "SourceSerif4Display-Light.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4Display-Light.ttf", + }, + { + file: "SourceSerif4Display-LightIt.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4Display-LightIt.ttf", + }, + // Semibold (600) — closest static cut to the website hero's runtime + // weight. The hero uses the variable font, which the browser + // interpolates to "Medium" (500) at the 60pt optical size; Adobe + // doesn't ship a static Medium Display variant, so we snap up to + // Semibold. Slightly heavier than the website but visibly closer + // than Light (300). + { + file: "SourceSerif4Display-Semibold.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4Display-Semibold.ttf", + }, + { + file: "SourceSerif4Display-SemiboldIt.ttf", + url: "https://cdn.jsdelivr.net/gh/adobe-fonts/source-serif@release/TTF/SourceSerif4Display-SemiboldIt.ttf", + }, + + // Tinos — Apache-2.0, metrically and visually compatible with Times + // New Roman. Used exclusively for the arrow glyph in the marketing + // headline. Pinning U+2192 to a TNR-equivalent font on BOTH the + // website and the OG card keeps the two visuals consistent + // regardless of which subset/fallback the runtime picks. This font + // is served to the browser too (via @font-face in tokens.css), so + // it lands in `public/fonts/` rather than `assets/fonts/`. + { + file: "Tinos-Regular.ttf", + url: "https://cdn.jsdelivr.net/gh/google/fonts@main/ofl/tinos/Tinos-Regular.ttf", + scope: "public", + }, + + // JetBrains Mono — for the eyebrow label. + { + file: "JetBrainsMono-Regular.ttf", + url: "https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@v2.304/fonts/ttf/JetBrainsMono-Regular.ttf", + }, + + // Caveat — for the hand-drawn alchemy.run URL stamp. + { + file: "Caveat-Regular.ttf", + url: "https://cdn.jsdelivr.net/gh/googlefonts/caveat@main/fonts/ttf/Caveat-Regular.ttf", + }, +]; + +async function exists(p: string): Promise { + try { + const s = await stat(p); + return s.size > 1024; + } catch { + return false; + } +} + +/** + * Derive a list of mirror URLs to try in order. jsdelivr's `cdn.jsdelivr.net/gh/` + * surface intermittently 403s under burst load; raw.githubusercontent.com serves + * the same blob directly from GitHub and is a reliable fallback. Any other URL + * is used as-is with no fallback. + */ +function mirrorsFor(url: string): string[] { + const m = url.match( + /^https:\/\/cdn\.jsdelivr\.net\/gh\/([^/]+)\/([^@]+)@([^/]+)\/(.+)$/, + ); + if (!m) return [url]; + const [, owner, repo, ref, rest] = m; + return [ + url, + `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${rest}`, + ]; +} + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +async function fetchWithRetry( + url: string, + attempts = 3, +): Promise { + let lastErr = ""; + for (let i = 0; i < attempts; i++) { + try { + const res = await fetch(url); + if (res.ok) { + const buf = new Uint8Array(await res.arrayBuffer()); + if (buf.byteLength < 1024) { + lastErr = `${buf.byteLength} bytes (suspiciously small)`; + } else { + return buf; + } + } else if (res.status === 404) { + // No point retrying a hard 404 — the URL itself is wrong. + return { error: `404 Not Found` }; + } else { + lastErr = `${res.status} ${res.statusText}`; + } + } catch (e) { + lastErr = (e as Error).message; + } + if (i < attempts - 1) await sleep(500 * 2 ** i); // 500ms, 1s + } + return { error: lastErr }; +} + +function dirFor(font: FontSource): string { + return font.scope === "public" ? publicFontsDir : buildOnlyFontsDir; +} + +async function downloadOne(font: FontSource): Promise<"cached" | "fetched"> { + const dest = path.join(dirFor(font), font.file); + if (await exists(dest)) return "cached"; + + const mirrors = mirrorsFor(font.url); + const errors: string[] = []; + for (const url of mirrors) { + const result = await fetchWithRetry(url); + if (result instanceof Uint8Array) { + await writeFile(dest, result); + return "fetched"; + } + errors.push(`${url} → ${result.error}`); + } + throw new Error( + `Failed to download ${font.file} from ${mirrors.length} mirror(s):\n ${errors.join("\n ")}`, + ); +} + +async function main() { + await mkdir(buildOnlyFontsDir, { recursive: true }); + await mkdir(publicFontsDir, { recursive: true }); + // Sequential, not parallel: jsdelivr rate-limits bursts of >5 concurrent + // requests from the same IP and starts returning 403s. Fonts are small + // and only fetched once per dev environment, so the speed difference + // is negligible. + let fetched = 0; + let cached = 0; + for (const font of FONTS) { + const r = await downloadOne(font); + if (r === "fetched") fetched++; + else cached++; + } + console.log( + `[fonts] ${fetched} downloaded, ${cached} cached → assets/fonts (build) + public/fonts (runtime)`, + ); +} + +await main(); diff --git a/.repos/alchemy-effect/website/scripts/generate-brand-assets.ts b/.repos/alchemy-effect/website/scripts/generate-brand-assets.ts new file mode 100644 index 00000000000..c4ac6105b3f --- /dev/null +++ b/.repos/alchemy-effect/website/scripts/generate-brand-assets.ts @@ -0,0 +1,156 @@ +/** + * Build-time brand asset generator. Runs before `astro build` and emits + * favicons + a fallback OG image into `website/public/`, all derived from + * the single yantra geometry source in `src/brand/yantra.ts`. + * + * The per-page OG images are rendered separately by the static endpoint at + * `src/pages/og/[...slug].png.ts` during `astro build`; this script only + * produces brand artifacts that need to exist on disk before Astro starts + * (so they're picked up by the public/ asset pipeline). + */ + +import { Resvg } from "@resvg/resvg-js"; +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { YANTRA_COLORS, yantraSvg } from "../src/brand/yantra.ts"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const publicDir = path.resolve(here, "../public"); + +/** Render an SVG string to PNG bytes at a target square size. */ +function rasterize(svg: string, size: number): Uint8Array { + const resvg = new Resvg(svg, { + fitTo: { mode: "width", value: size }, + background: "rgba(0, 0, 0, 0)", + }); + return resvg.render().asPng(); +} + +/** + * A favicon-friendly variant of the yantra: parchment tile with a small + * inner padding so the glyph reads well at 16px. The stroke weight is bumped + * because the lines collapse below ~1.4 viewBox units when downscaled. + */ +function faviconTileSvg(): string { + return yantraSvg({ + size: 64, + bg: YANTRA_COLORS.bg, + stroke: YANTRA_COLORS.stroke, + dot: YANTRA_COLORS.dot, + strokeWidth: 1.4, + }); +} + +/** + * apple-touch-icon needs an opaque background and generous padding — + * iOS renders it inside its own rounded-rect mask. + */ +function appleTouchSvg(): string { + // Embed the standard 24-unit yantra centered inside a 32-unit padded canvas. + const inner = yantraSvg({ + size: 24, + stroke: YANTRA_COLORS.stroke, + dot: YANTRA_COLORS.dot, + strokeWidth: 1.1, + }); + // Strip the outer wrapper so we can re-mount the geometry inside a + // padded canvas — easier than computing translate() in two places. + const innerBody = inner.replace(/^]*>/, "").replace(/<\/svg>$/, ""); + return ` + + ${innerBody} + `; +} + +/** + * Static OG fallback (1200×630). Simple, hand-crafted SVG so this script + * has no satori/font dependency. Used when a page has no slug-specific OG + * image (e.g. external referrers hitting the bare domain). + */ +function ogFallbackSvg(): string { + const W = 1200; + const H = 630; + // Yantra glyph centered, large. + const glyphSize = 220; + const glyph = yantraSvg({ + size: glyphSize, + stroke: YANTRA_COLORS.stroke, + dot: YANTRA_COLORS.dot, + strokeWidth: 0.7, + }) + .replace(/^]*>/, "") + .replace(/<\/svg>$/, ""); + + return ` + + + + + + ${glyph} + + + alchemy + + ZERO → PRODUCTION + + + alchemy.run + `; +} + +async function main() { + await mkdir(publicDir, { recursive: true }); + + // 1. Vector favicon — parchment tile, bumped stroke for tab legibility. + const favSvg = faviconTileSvg(); + await writeFile(path.join(publicDir, "favicon.svg"), favSvg); + + // 2. Raster favicons. + await writeFile( + path.join(publicDir, "favicon-32.png"), + rasterize(favSvg, 32), + ); + await writeFile( + path.join(publicDir, "favicon-16.png"), + rasterize(favSvg, 16), + ); + + // 3. apple-touch-icon (180×180, padded, opaque parchment). + const apple = appleTouchSvg(); + await writeFile( + path.join(publicDir, "apple-touch-icon.png"), + rasterize(apple, 180), + ); + + // 4. Larger PWA / share fallback at 512×512. + await writeFile(path.join(publicDir, "icon-512.png"), rasterize(apple, 512)); + + // 5. Backwards-compat: keep the old /favicon.png reference (used by + // some cached nav code) pointing to the 32px raster. + await writeFile(path.join(publicDir, "favicon.png"), rasterize(favSvg, 32)); + + // 6. OG fallback (1200×630). Per-page OG images come from the static + // endpoint; this is the bare-domain fallback. + const ogSvg = ogFallbackSvg(); + await writeFile(path.join(publicDir, "og-default.svg"), ogSvg); + await writeFile( + path.join(publicDir, "og-default.png"), + rasterize(ogSvg, 1200), + ); + + // eslint-disable-next-line no-console + console.log( + "[brand] wrote favicon.{svg,png}, favicon-{16,32}.png, apple-touch-icon.png, icon-512.png, og-default.{svg,png}", + ); +} + +await main(); diff --git a/.repos/alchemy-effect/website/scripts/generate-llms-txt.ts b/.repos/alchemy-effect/website/scripts/generate-llms-txt.ts new file mode 100644 index 00000000000..020326f731e --- /dev/null +++ b/.repos/alchemy-effect/website/scripts/generate-llms-txt.ts @@ -0,0 +1,275 @@ +/** + * Generates `public/llms.txt` — a navigation index of every docs page, + * grouped by section, with title + description pulled from each page's + * frontmatter. + * + * Run with: `bun scripts/generate-llms-txt.ts` + * + * Section ordering, headings, and prose intros are configured here. + * Page metadata (title, description) comes from the source frontmatter, + * so editing a page's frontmatter is enough to update llms.txt. + */ + +import { readdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const docsDir = path.resolve(here, "../src/content/docs"); +const outFile = path.resolve(here, "../public/llms.txt"); +const siteUrl = "https://v2.alchemy.run"; + +interface Page { + /** URL path, e.g. "/concepts/binding" */ + href: string; + /** Path relative to docs dir without extension, e.g. "concepts/binding" */ + slug: string; + title: string; + description: string; + draft: boolean; + /** `sidebar.order` from frontmatter; `Infinity` when unset. */ + order: number; +} + +interface Section { + /** H2 heading */ + heading: string; + /** Optional prose paragraph after the heading. */ + intro?: string; + /** + * Pages to include. Either a list of explicit slugs (relative to docs dir, + * no extension) in the desired order, or a directory to enumerate + * alphabetically. + */ + pages: { slugs: string[] } | { directory: string; exclude?: string[] }; +} + +const SECTIONS: Section[] = [ + { + heading: "Start here", + pages: { slugs: ["what-is-alchemy", "getting-started"] }, + }, + { + heading: "Tutorial — main path (Cloudflare)", + intro: + "A linear five-part walkthrough from zero to a tested, locally-developed, CI-deployed Cloudflare project. Each part builds on the previous one.", + pages: { + slugs: [ + "tutorial/part-1", + "tutorial/part-2", + "tutorial/part-3", + "tutorial/part-4", + "tutorial/part-5", + ], + }, + }, + { + heading: "Tutorial — Cloudflare add-ons", + intro: + "Standalone tutorials that extend the main tutorial's Worker with a specific Cloudflare feature. Pick the ones that match your use case.", + pages: { directory: "tutorial/cloudflare" }, + }, + { + heading: "Tutorial — AWS", + intro: + "End-to-end AWS tutorials. Read the Lambda page first; the others bind storage and event sources to that Lambda.", + pages: { directory: "tutorial/aws" }, + }, + { + heading: "Concepts — the mental model", + intro: + "Reference pages explaining what each primitive means and how they fit together. Read these when something in a tutorial feels magical, or before designing a new Stack.", + pages: { directory: "concepts" }, + }, + { + heading: "Guides — task-oriented", + intro: + "Standalone how-to pages. Each solves a specific problem; read in any order.", + pages: { directory: "guides" }, + }, +]; + +const PROVIDERS_INTRO = `Per-resource API reference, generated from JSDoc on the source \`.ts\` files via \`bun generate:api-reference\`. Each page documents the resource's input properties (with types, defaults, and constraints), output attributes, and Quick Reference / Examples sections derived from \`@section\` / \`@example\` JSDoc tags. Grouped by cloud below.`; + +/** + * Enumerates every generated provider page under `providers/{Cloud}/...`, + * grouped by cloud. These pages are produced by `build:reference` (which runs + * before this script in the build), so they exist on disk at generation time + * even though they are gitignored. + */ +async function renderProvidersSection(): Promise { + const providersDir = path.join(docsDir, "providers"); + let clouds: string[]; + try { + const entries = await readdir(providersDir, { withFileTypes: true }); + clouds = entries + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + } catch (err: any) { + if (err?.code === "ENOENT") return `## Providers\n\n${PROVIDERS_INTRO}`; + throw err; + } + + const blocks: string[] = [`## Providers`, PROVIDERS_INTRO]; + for (const cloud of clouds) { + const slugs = await listSlugs(`providers/${cloud}`); + const pages = (await Promise.all(slugs.map(loadPage))) + .filter((p) => !p.draft) + // Starlight serves provider routes lowercased (e.g. the CamelCase source + // `providers/AWS/S3/Bucket.md` is reachable at `/providers/aws/s3/bucket`). + .map((p) => ({ ...p, href: p.href.toLowerCase() })) + .sort((a, b) => a.title.localeCompare(b.title)); + if (pages.length === 0) continue; + blocks.push(`### ${cloud}`); + blocks.push(pages.map(renderPage).join("\n")); + } + return blocks.join("\n\n"); +} + +const HEADER = `# Alchemy + +> Alchemy Effect is an Infrastructure-as-Effects (IaE) framework that combines cloud infrastructure and application logic into a single, type-safe program powered by [Effect](https://effect.website). Resources are declared as Effects; bindings wire IAM, env vars, and typed SDKs in one call; deploys and runtime share the same code. + +This file is a navigation index for the documentation site at ${siteUrl}. Every page under \`/src/content/docs/\` is listed below with its URL and a one-line summary, so an agent can pick the right page in one hop.`; + +function parseFrontmatter(source: string): Record { + if (!source.startsWith("---")) return {}; + const end = source.indexOf("\n---", 3); + if (end === -1) return {}; + const block = source.slice(3, end); + const out: Record = {}; + for (const rawLine of block.split("\n")) { + const line = rawLine.trimEnd(); + const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/); + if (!m) continue; + let value = m[2].trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + out[m[1]] = value; + } + return out; +} + +/** + * Extracts the nested `sidebar.order` value from a frontmatter block. + * Starlight uses this to order autogenerated sidebar groups; we mirror it + * so llms.txt lists pages in the same order the sidebar shows them. + * Returns `Infinity` when unset, so unordered pages sort after ordered ones. + */ +function parseSidebarOrder(source: string): number { + if (!source.startsWith("---")) return Number.POSITIVE_INFINITY; + const end = source.indexOf("\n---", 3); + if (end === -1) return Number.POSITIVE_INFINITY; + const block = source.slice(3, end); + const lines = block.split("\n"); + for (let i = 0; i < lines.length; i++) { + if (!/^sidebar:\s*$/.test(lines[i])) continue; + for (let j = i + 1; j < lines.length; j++) { + if (/^\S/.test(lines[j])) break; // dedented out of the sidebar block + const m = lines[j].match(/^\s+order:\s*(-?[\d.]+)\s*$/); + if (m) return Number.parseFloat(m[1]); + } + break; + } + return Number.POSITIVE_INFINITY; +} + +async function loadPage(slug: string): Promise { + const candidates = [`${slug}.mdx`, `${slug}.md`]; + for (const rel of candidates) { + const full = path.join(docsDir, rel); + try { + const source = await readFile(full, "utf8"); + const fm = parseFrontmatter(source); + const title = fm.title; + const description = fm.description ?? fm.excerpt ?? ""; + if (!title) { + throw new Error(`Missing title in frontmatter: ${rel}`); + } + return { + href: `/${slug}`, + slug, + title, + description, + draft: fm.draft === "true" || (fm.draft as unknown as boolean) === true, + order: parseSidebarOrder(source), + }; + } catch (err: any) { + if (err?.code !== "ENOENT") throw err; + } + } + throw new Error(`Page not found: ${slug} (looked for .mdx and .md)`); +} + +async function listSlugs( + directory: string, + exclude: string[] = [], +): Promise { + const dir = path.join(docsDir, directory); + const entries = await readdir(dir, { withFileTypes: true }); + const slugs: string[] = []; + for (const entry of entries) { + const rel = `${directory}/${entry.name}`; + if (entry.isDirectory()) { + slugs.push(...(await listSlugs(rel, exclude))); + continue; + } + if (!entry.isFile()) continue; + const ext = path.extname(entry.name); + if (ext !== ".md" && ext !== ".mdx") continue; + const slug = `${directory}/${entry.name.slice(0, -ext.length)}`; + if (exclude.includes(slug)) continue; + slugs.push(slug); + } + slugs.sort(); + return slugs; +} + +function byOrderThenTitle(a: Page, b: Page): number { + if (a.order !== b.order) return a.order - b.order; + return a.title.localeCompare(b.title); +} + +function renderPage(page: Page): string { + const url = `${siteUrl}${page.href}`; + const desc = page.description ? ` — ${page.description}` : ""; + return `- [${page.title}](${url})${desc}`; +} + +async function main() { + const parts: string[] = [HEADER]; + + for (const section of SECTIONS) { + const isDirectory = !("slugs" in section.pages); + const slugs = + "slugs" in section.pages + ? section.pages.slugs + : await listSlugs(section.pages.directory, section.pages.exclude); + const pages = (await Promise.all(slugs.map(loadPage))).filter( + (p) => !p.draft, + ); + // Directory sections mirror the sidebar's `sidebar.order` ordering; slug + // sections keep the curated order they were declared in. + if (isDirectory) pages.sort(byOrderThenTitle); + + parts.push(`## ${section.heading}`); + if (section.intro) parts.push(section.intro); + parts.push(pages.map(renderPage).join("\n")); + } + + parts.push(await renderProvidersSection()); + + const body = parts.join("\n\n") + "\n"; + await writeFile(outFile, body, "utf8"); + console.log(`Wrote ${outFile}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/.repos/alchemy-effect/website/scripts/generate-readme-hero.ts b/.repos/alchemy-effect/website/scripts/generate-readme-hero.ts new file mode 100644 index 00000000000..69c50320327 --- /dev/null +++ b/.repos/alchemy-effect/website/scripts/generate-readme-hero.ts @@ -0,0 +1,89 @@ +/** + * Renders the repo-root README hero image. Run once (or whenever the + * brand mark / wordmark changes) and the resulting PNG is committed to + * `images/readme-hero.png`. GitHub serves it straight out of the tree. + * + * Pipeline mirrors the per-page OG image endpoint + * (`src/pages/og/[...slug].png.ts`): satori → SVG → resvg → PNG, with + * the same static TTF fonts loaded from `website/assets/fonts/` so the + * artwork uses the website's headline face (Source Serif 4 Display) + * instead of a fallback. + */ + +import { Resvg } from "@resvg/resvg-js"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import satori from "satori"; +import { + README_HERO_H, + README_HERO_W, + ReadmeHero, +} from "../src/brand/ReadmeHero.tsx"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const fontsDir = path.resolve(here, "../assets/fonts"); +const outDir = path.resolve(here, "../../images"); +const outFile = path.join(outDir, "readme-hero.png"); + +async function font(file: string): Promise { + return readFile(path.join(fontsDir, file)); +} + +async function main() { + const [serifIt, displayLightIt, displaySemiIt, displayReg, mono, caveat] = + await Promise.all([ + font("SourceSerif4-It.ttf"), + font("SourceSerif4Display-LightIt.ttf"), + font("SourceSerif4Display-SemiboldIt.ttf"), + font("SourceSerif4Display-Regular.ttf"), + font("JetBrainsMono-Regular.ttf"), + font("Caveat-Regular.ttf"), + ]); + + const fonts = [ + { name: "Source Serif 4", data: serifIt, weight: 400, style: "italic" }, + { + name: "Source Serif 4 Display", + data: displayLightIt, + weight: 300, + style: "italic", + }, + { + name: "Source Serif 4 Display", + data: displayReg, + weight: 400, + style: "normal", + }, + { + name: "Source Serif 4 Display", + data: displaySemiIt, + weight: 600, + style: "italic", + }, + { name: "JetBrains Mono", data: mono, weight: 400, style: "normal" }, + { name: "Caveat", data: caveat, weight: 400, style: "normal" }, + ] as const; + + const svg = await satori(ReadmeHero(), { + width: README_HERO_W, + height: README_HERO_H, + fonts: fonts as any, + }); + + const png = new Resvg(svg, { + fitTo: { mode: "width", value: README_HERO_W }, + }) + .render() + .asPng(); + + await mkdir(outDir, { recursive: true }); + await writeFile(outFile, png); + + // eslint-disable-next-line no-console + console.log( + `[readme-hero] wrote ${path.relative(process.cwd(), outFile)} (${README_HERO_W}×${README_HERO_H}, ${(png.byteLength / 1024).toFixed(1)} KiB)`, + ); +} + +await main(); diff --git a/.repos/alchemy-effect/website/scripts/preview-og.ts b/.repos/alchemy-effect/website/scripts/preview-og.ts new file mode 100644 index 00000000000..febb5a9dc6d --- /dev/null +++ b/.repos/alchemy-effect/website/scripts/preview-og.ts @@ -0,0 +1,109 @@ +/** + * One-shot preview generator for the blog OG card. Renders a sample + * card to /tmp/og-preview.png using the same satori + resvg pipeline + * as the production endpoint. + */ +import { Resvg } from "@resvg/resvg-js"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import satori from "satori"; +import { OgCard } from "../src/brand/OgCard"; + +const fontsDir = fileURLToPath(new URL("../assets/fonts/", import.meta.url)); +const publicFontsDir = fileURLToPath( + new URL("../public/fonts/", import.meta.url), +); +const read = (name: string, pub = false) => + fs.readFile(path.join(pub ? publicFontsDir : fontsDir, name)); + +const [ + serif, + serifIt, + displayLight, + displayLightIt, + displayReg, + displayRegIt, + displaySemi, + displaySemiIt, + tinos, + mono, + caveat, +] = await Promise.all([ + read("SourceSerif4-Regular.ttf"), + read("SourceSerif4-It.ttf"), + read("SourceSerif4Display-Light.ttf"), + read("SourceSerif4Display-LightIt.ttf"), + read("SourceSerif4Display-Regular.ttf"), + read("SourceSerif4Display-It.ttf"), + read("SourceSerif4Display-Semibold.ttf"), + read("SourceSerif4Display-SemiboldIt.ttf"), + read("Tinos-Regular.ttf", true), + read("JetBrainsMono-Regular.ttf"), + read("Caveat-Regular.ttf"), +]); + +const fonts = [ + { name: "Source Serif 4", data: serif, weight: 400, style: "normal" }, + { name: "Source Serif 4", data: serifIt, weight: 400, style: "italic" }, + { + name: "Source Serif 4 Display", + data: displayLight, + weight: 300, + style: "normal", + }, + { + name: "Source Serif 4 Display", + data: displayLightIt, + weight: 300, + style: "italic", + }, + { + name: "Source Serif 4 Display", + data: displayReg, + weight: 400, + style: "normal", + }, + { + name: "Source Serif 4 Display", + data: displayRegIt, + weight: 400, + style: "italic", + }, + { + name: "Source Serif 4 Display", + data: displaySemi, + weight: 600, + style: "normal", + }, + { + name: "Source Serif 4 Display", + data: displaySemiIt, + weight: 600, + style: "italic", + }, + { name: "Tinos", data: tinos, weight: 400, style: "normal" }, + { name: "JetBrains Mono", data: mono, weight: 400, style: "normal" }, + { name: "Caveat", data: caveat, weight: 400, style: "normal" }, +] as const; + +const element = OgCard({ + kind: "blog", + title: "What's new in beta.39", + description: + "A small, high-impact fix release — VITE_* env props are now inlined into the client bundle, the Cloudflare Worker HTTP adapter runs handlers through Effect's standard HTTP lifecycle (unblocking RpcServer.toHttpEffect), and the SendEmail binding from beta.38 is now wired into Worker binding inference.", + date: "2026-05-13", +}); + +const svg = await satori(element, { + width: 1200, + height: 630, + fonts: fonts as any, +}); +const png = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } }) + .render() + .asPng(); + +const out = "/tmp/og-preview.png"; +await fs.writeFile(out, png); +console.log(`wrote ${out}`); diff --git a/.repos/alchemy-effect/website/src/blog-sidebar.ts b/.repos/alchemy-effect/website/src/blog-sidebar.ts new file mode 100644 index 00000000000..cdd4be44914 --- /dev/null +++ b/.repos/alchemy-effect/website/src/blog-sidebar.ts @@ -0,0 +1,77 @@ +/// +import { defineRouteMiddleware } from "@astrojs/starlight/route-data"; +import type { StarlightRouteData } from "@astrojs/starlight/route-data"; +import { getCollection } from "astro:content"; +import type { BlogCategory } from "./content.config"; + +type SidebarItem = StarlightRouteData["sidebar"][number]; +type SidebarGroup = Extract; + +const groupLabels: Record = { + release: "Releases", + post: "Posts", +}; +const groupOrder: BlogCategory[] = ["release", "post"]; + +let categoryById: Map | undefined; + +async function loadCategoryById(): Promise> { + if (categoryById) return categoryById; + const entries = await getCollection("docs", (entry) => + entry.id.startsWith("blog/"), + ); + const map = new Map(); + for (const entry of entries) { + const data = entry.data as { category?: BlogCategory }; + map.set(entry.id, data.category ?? "post"); + } + categoryById = map; + return map; +} + +function extractBlogId(href: string): string | undefined { + const match = href.match(/\/blog\/([^/]+)\/?$/); + return match ? `blog/${match[1]}` : undefined; +} + +export const onRequest = defineRouteMiddleware(async (context, next) => { + await next(); + + const { starlightRoute, t } = context.locals; + const recentLabel = t("starlightBlog.sidebar.recent"); + + const recentIndex = starlightRoute.sidebar.findIndex( + (item): item is SidebarGroup => + item.type === "group" && item.label === recentLabel, + ); + if (recentIndex === -1) return; + + const recentGroup = starlightRoute.sidebar[recentIndex] as SidebarGroup; + const categories = await loadCategoryById(); + + const buckets = new Map(); + for (const category of groupOrder) buckets.set(category, []); + + for (const item of recentGroup.entries) { + if (item.type !== "link") continue; + const id = extractBlogId(item.href); + const category: BlogCategory = + (id !== undefined ? categories.get(id) : undefined) ?? "post"; + buckets.get(category)!.push(item); + } + + const replacement: SidebarGroup[] = []; + for (const category of groupOrder) { + const entries = buckets.get(category)!; + if (entries.length === 0) continue; + replacement.push({ + type: "group", + label: groupLabels[category], + entries, + collapsed: false, + badge: undefined, + }); + } + + starlightRoute.sidebar.splice(recentIndex, 1, ...replacement); +}); diff --git a/.repos/alchemy-effect/website/src/brand/OgCard.tsx b/.repos/alchemy-effect/website/src/brand/OgCard.tsx new file mode 100644 index 00000000000..92609b694cc --- /dev/null +++ b/.repos/alchemy-effect/website/src/brand/OgCard.tsx @@ -0,0 +1,446 @@ +/** + * Satori template for Open Graph cards. Consumed only by the static + * `og/[...slug].png.ts` endpoint at build time — never shipped to the + * browser. The JSX is interpreted by satori, which supports a Flexbox + * subset and inline `style` props (no CSS classes). + * + * Two visual variants: + * + * - `doc` / `marketing` — parchment background, serif headline, the + * yantra glyph + eyebrow up top, hand-drawn "alchemy.run" caption + * bottom-right. Mirrors the homepage hero. + * + * - `blog` — dark variant inspired by Bun's release-note cards. Title + * anchored top-left, dense multi-line description filling the body, + * publish date + yantra mark in the footer. Designed to look full + * and editorial; relies on posts having a meaty `description`/ + * `excerpt` in frontmatter. + * + * Title and description are rendered verbatim from the source page's + * frontmatter — no splitting, truncation, or glyph workarounds. The + * full unsubsetted variable TTFs loaded by the endpoint cover every + * Unicode codepoint we use. + */ + +import { yantraSvg } from "./yantra"; + +const COLORS = { + bg: "#f5efe3", + fg1: "#2a2620", + fg2: "#4e402c", + fg3: "#85714f", + accent: "#5c7a3e", + accentDeep: "#3f5a2a", + hairline: "rgba(42,38,32,0.14)", + // Blog (dark) palette. + darkBg: "#161310", + darkFg1: "#f5efe3", + darkFg2: "#bdb09a", + darkFg3: "#7d705c", + darkAccent: "#a8c47a", + // Dark-mode yantra — lifted moss stroke + terracotta bindu dot, mirroring + // the runtime tokens (--alc-accent / --alc-yantra-dot in the .dark block). + darkYantraStroke: "#7a9a5e", + darkYantraDot: "#c56e3c", + darkHairline: "rgba(245,239,227,0.12)", +} as const; + +export type OgCardKind = "marketing" | "doc" | "blog"; + +/** + * One styled segment of a structured title — mirrors the way the + * homepage hero declares its own emphasis with explicit `` markup. + * Pages that want the accent treatment supply an array; doc pages pass + * a plain string and get plain text. + */ +export interface TitlePart { + text: string; + italic?: boolean; + /** Render this part in the deep-moss accent color. */ + accent?: boolean; + /** + * Override the font family for this part. Use `"tinos"` for glyphs + * that should render from the TNR-equivalent face (the marketing + * arrow `→` — mirrors the website's font stack falling through to + * Times New Roman for U+2192). Default: Source Serif 4 Display. + */ + font?: "tinos"; +} + +export interface OgCardProps { + title: string | TitlePart[]; + description?: string; + /** Drives the eyebrow label (e.g. "guide", "concept", "blog"). */ + eyebrow?: string; + kind?: OgCardKind; + /** ISO date string (YYYY-MM-DD). Rendered in the blog footer. */ + date?: string; +} + +const W = 1200; +const H = 630; + +export function OgCard(props: OgCardProps): any { + const kind = props.kind ?? "doc"; + if (kind === "blog") return BlogCard(props); + return DocCard(props); +} + +// ──────────────────────────────────────────────────────────────────────────── +// Doc / marketing variant — parchment hero. +// ──────────────────────────────────────────────────────────────────────────── + +function DocCard({ title, description, eyebrow, kind }: OgCardProps): any { + const eyebrowText = (eyebrow ?? defaultEyebrow(kind ?? "doc")).toUpperCase(); + const yantraDataUrl = yantraImage(COLORS.accentDeep); + + return { + type: "div", + key: null, + props: { + style: { + width: W, + height: H, + display: "flex", + flexDirection: "column", + backgroundColor: COLORS.bg, + padding: "56px 64px", + fontFamily: "Source Serif 4", + color: COLORS.fg1, + position: "relative", + }, + children: [ + // Eyebrow row — yantra mark + monospace label. + { + type: "div", + key: "top", + props: { + style: { display: "flex", alignItems: "center", gap: 18 }, + children: [ + { + type: "img", + key: "y", + props: { + src: yantraDataUrl, + width: 56, + height: 56, + style: { display: "flex" }, + }, + }, + { + type: "div", + key: "eb", + props: { + style: { + fontFamily: "JetBrains Mono", + fontSize: 18, + letterSpacing: 3, + color: COLORS.accentDeep, + fontWeight: 400, + }, + children: eyebrowText, + }, + }, + ], + }, + }, + // Title. + { + type: "div", + key: "title", + props: { + style: { + display: "flex", + flexWrap: "wrap", + alignItems: "baseline", + marginTop: 56, + fontFamily: "Source Serif 4 Display", + fontWeight: 600, + fontSize: 110, + lineHeight: 1.02, + letterSpacing: -2, + color: COLORS.fg1, + }, + children: renderTitle(title, COLORS.fg1, COLORS.accentDeep), + }, + }, + description + ? { + type: "div", + key: "desc", + props: { + style: { + display: "flex", + marginTop: 36, + fontSize: 26, + lineHeight: 1.45, + color: COLORS.fg2, + maxWidth: 980, + }, + children: description, + }, + } + : null, + // Spacer pushes the footer to the bottom. + { + type: "div", + key: "spacer", + props: { style: { display: "flex", flexGrow: 1 } }, + }, + // Footer — hairline + wordmark + hand-drawn URL. + { + type: "div", + key: "footer", + props: { + style: { + display: "flex", + alignItems: "flex-end", + justifyContent: "space-between", + borderTop: `1px solid ${COLORS.hairline}`, + paddingTop: 24, + }, + children: [ + { + type: "div", + key: "wm", + props: { + style: { + fontFamily: "Source Serif 4", + fontStyle: "italic", + fontWeight: 400, + fontSize: 32, + color: COLORS.fg1, + }, + children: "alchemy", + }, + }, + { + type: "div", + key: "url", + props: { + style: { + fontFamily: "Caveat", + fontWeight: 400, + fontSize: 36, + color: COLORS.accentDeep, + }, + children: "alchemy.run", + }, + }, + ], + }, + }, + ].filter(Boolean), + }, + }; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Blog variant — dark, dense, Bun-inspired release card. +// ──────────────────────────────────────────────────────────────────────────── + +function BlogCard({ title, description, date }: OgCardProps): any { + const yantraDataUrl = yantraImage( + COLORS.darkYantraStroke, + COLORS.darkYantraDot, + ); + + return { + type: "div", + key: null, + props: { + style: { + width: W, + height: H, + display: "flex", + flexDirection: "column", + backgroundColor: COLORS.darkBg, + padding: "72px 80px", + fontFamily: "Source Serif 4", + color: COLORS.darkFg1, + }, + children: [ + // Title — large, top-anchored. Plain string for blog posts. + { + type: "div", + key: "title", + props: { + style: { + display: "flex", + flexWrap: "wrap", + fontFamily: "Source Serif 4 Display", + fontWeight: 600, + fontSize: 84, + lineHeight: 1.05, + letterSpacing: -1.5, + color: COLORS.darkFg1, + }, + children: renderTitle(title, COLORS.darkFg1, COLORS.darkAccent), + }, + }, + // Description — fills the body. Larger maxWidth than doc cards + // so multi-sentence excerpts wrap to 4–6 lines. + description + ? { + type: "div", + key: "desc", + props: { + style: { + display: "flex", + marginTop: 32, + fontSize: 28, + lineHeight: 1.5, + color: COLORS.darkFg2, + maxWidth: 1040, + }, + children: description, + }, + } + : null, + // Spacer pushes the footer to the bottom. + { + type: "div", + key: "spacer", + props: { style: { display: "flex", flexGrow: 1 } }, + }, + // Footer — date on the left, yantra mark on the right. + { + type: "div", + key: "footer", + props: { + style: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + borderTop: `1px solid ${COLORS.darkHairline}`, + paddingTop: 28, + }, + children: [ + { + type: "div", + key: "date", + props: { + style: { + display: "flex", + flexDirection: "column", + gap: 4, + }, + children: [ + { + type: "div", + key: "d", + props: { + style: { + fontFamily: "Source Serif 4", + fontSize: 24, + color: COLORS.darkFg2, + }, + children: formatDate(date), + }, + }, + { + type: "div", + key: "wm", + props: { + style: { + fontFamily: "JetBrains Mono", + fontSize: 16, + letterSpacing: 3, + color: COLORS.darkFg3, + }, + children: "ALCHEMY.RUN", + }, + }, + ], + }, + }, + { + type: "img", + key: "y", + props: { + src: yantraDataUrl, + width: 72, + height: 72, + style: { display: "flex" }, + }, + }, + ], + }, + }, + ].filter(Boolean), + }, + }; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Helpers +// ──────────────────────────────────────────────────────────────────────────── + +function yantraImage(stroke: string, dot: string = stroke): string { + const svg = yantraSvg({ + size: 96, + stroke, + dot, + strokeWidth: 0.7, + }); + return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`; +} + +function renderTitle( + title: string | TitlePart[], + fg: string, + accent: string, +): any { + if (!Array.isArray(title)) return title; + return title.map((part, i) => ({ + type: "span", + key: `tp${i}`, + props: { + style: { + fontFamily: part.font === "tinos" ? "Tinos" : "Source Serif 4 Display", + fontStyle: part.italic ? "italic" : "normal", + color: part.accent ? accent : fg, + fontWeight: part.font === "tinos" ? 400 : 600, + }, + children: part.text, + }, + })); +} + +const MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +function formatDate(iso: string | undefined): string { + if (!iso) return ""; + // Parse YYYY-MM-DD without timezone surprises. + const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso); + if (!m) return iso; + const year = Number(m[1]); + const month = Number(m[2]); + const day = Number(m[3]); + return `${MONTHS[month - 1]} ${day}, ${year}`; +} + +function defaultEyebrow(kind: OgCardKind): string { + switch (kind) { + case "marketing": + return "alchemy · zero to production"; + case "blog": + return "blog · alchemy.run"; + case "doc": + default: + return "alchemy · documentation"; + } +} diff --git a/.repos/alchemy-effect/website/src/brand/ReadmeHero.tsx b/.repos/alchemy-effect/website/src/brand/ReadmeHero.tsx new file mode 100644 index 00000000000..3849b4db724 --- /dev/null +++ b/.repos/alchemy-effect/website/src/brand/ReadmeHero.tsx @@ -0,0 +1,78 @@ +/** + * Satori template for the README hero — a tight stacked lockup of the + * yantra mark above the italic "Alchemy" wordmark, centered on a + * parchment ground. No frame, no tagline, no URL stamp. Rendered offline + * by `scripts/generate-readme-hero.ts` to `images/readme-hero.png`. + * + * The native render size is 1200×720 (5:3). The README displays it at a + * fixed badge width (~360px) so it reads as a brand mark, not a banner. + */ + +import { yantraSvg } from "./yantra"; + +const COLORS = { + bg: "#f5efe3", + fg: "#2a2620", + accent: "#3f5a2a", +} as const; + +export const README_HERO_W = 1200; +export const README_HERO_H = 720; + +export function ReadmeHero(): any { + const yantra = yantraSvg({ + size: 280, + stroke: COLORS.accent, + dot: COLORS.accent, + strokeWidth: 0.7, + }); + const yantraDataUrl = `data:image/svg+xml;base64,${Buffer.from(yantra).toString("base64")}`; + + return { + type: "div", + key: null, + props: { + style: { + width: README_HERO_W, + height: README_HERO_H, + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + gap: 16, + backgroundColor: COLORS.bg, + fontFamily: "Source Serif 4 Display", + color: COLORS.fg, + }, + children: [ + { + type: "img", + key: "yantra", + props: { + src: yantraDataUrl, + width: 280, + height: 280, + style: { display: "flex" }, + }, + }, + { + type: "div", + key: "wm", + props: { + style: { + display: "flex", + fontFamily: "Source Serif 4 Display", + fontStyle: "italic", + fontWeight: 600, + fontSize: 280, + lineHeight: 1, + letterSpacing: -4, + color: COLORS.fg, + }, + children: "Alchemy", + }, + }, + ], + }, + }; +} diff --git a/.repos/alchemy-effect/website/src/brand/Yantra.astro b/.repos/alchemy-effect/website/src/brand/Yantra.astro new file mode 100644 index 00000000000..39043b20147 --- /dev/null +++ b/.repos/alchemy-effect/website/src/brand/Yantra.astro @@ -0,0 +1,39 @@ +--- +import { yantraSvg } from "./yantra"; + +interface Props { + size?: number; + stroke?: string; + dot?: string; + bg?: string; + strokeWidth?: number; + /** Inherit color from surrounding text (default true for inline use). */ + useCurrentColor?: boolean; + class?: string; +} + +const { + size = 42, + stroke, + dot, + bg, + strokeWidth, + useCurrentColor = true, + class: className, +} = Astro.props; + +// Theme-aware bindu: green in light, terracotta in dark. Defined per +// theme in tokens.css; falls back to deep moss outside the website. +const resolvedDot = dot ?? "var(--alc-yantra-dot, #3f5a2a)"; + +const svg = yantraSvg({ + size, + stroke, + dot: resolvedDot, + bg, + strokeWidth, + useCurrentColor, +}); +--- + +