Initial bemo worker setup (#4017)
Sets up preview deploys etc. for bemo worker. There's enough going on here that I wanted to make it its own PR. I'll rework david's spike on top of it once it's landed. ### Change Type - [x] `internal` — Does not affect user-facing stuff - [x] `chore` — Updating dependencies, other boring stuff --------- Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
This commit is contained in:
parent
cafa0f5636
commit
57fb7a0650
55 changed files with 1278 additions and 719 deletions
54
.github/workflows/deploy-bemo.yml
vendored
Normal file
54
.github/workflows/deploy-bemo.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
name: Deploy bemo
|
||||||
|
|
||||||
|
# TODO: add some sort of production trigger
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: 1
|
||||||
|
PRINT_GITHUB_ANNOTATIONS: 1
|
||||||
|
TLDRAW_ENV: ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy bemo to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
runs-on: ubuntu-latest-16-cores-open
|
||||||
|
environment: ${{ github.ref == 'refs/heads/production' && 'bemo-production' || 'bemo-canary' }}
|
||||||
|
concurrency: bemo-${{ github.ref == 'refs/heads/production' && 'production' || github.ref }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify initial start
|
||||||
|
uses: MineBartekSA/discord-webhook@v2
|
||||||
|
if: github.ref == 'refs/heads/production'
|
||||||
|
with:
|
||||||
|
webhook: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
|
||||||
|
content: 'Preparing ${{ env.TLDRAW_ENV }} bemo deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup
|
||||||
|
|
||||||
|
- name: Build types
|
||||||
|
run: yarn build-types
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: yarn tsx scripts/deploy-bemo.ts
|
||||||
|
env:
|
||||||
|
RELEASE_COMMIT_HASH: ${{ github.sha }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
DISCORD_DEPLOY_WEBHOOK_URL: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_BEMO_WORKER_DSN: ${{ secrets.SENTRY_BEMO_WORKER_DSN }}
|
|
@ -1,4 +1,4 @@
|
||||||
name: Deploy
|
name: Deploy .com
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -17,11 +17,11 @@ defaults:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
name: Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
runs-on: ubuntu-latest-16-cores-open
|
runs-on: ubuntu-latest-16-cores-open
|
||||||
environment: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || 'deploy-staging' }}
|
environment: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || 'deploy-staging' }}
|
||||||
concurrency: ${{ github.ref == 'refs/heads/production' && 'deploy-production' || github.ref }}
|
concurrency: dotcom-${{ github.ref == 'refs/heads/production' && 'deploy-production' || github.ref }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Notify initial start
|
- name: Notify initial start
|
||||||
|
@ -29,14 +29,7 @@ jobs:
|
||||||
if: github.ref == 'refs/heads/production'
|
if: github.ref == 'refs/heads/production'
|
||||||
with:
|
with:
|
||||||
webhook: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
|
webhook: ${{ secrets.DISCORD_DEPLOY_WEBHOOK_URL }}
|
||||||
content: 'Preparing ${{ env.TLDRAW_ENV }} deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
|
content: 'Preparing ${{ env.TLDRAW_ENV }} dotcom deploy: ${{ github.event.head_commit.message }} by ${{ github.event.head_commit.author.name }}'
|
||||||
component: |
|
|
||||||
{
|
|
||||||
"type": 2,
|
|
||||||
"style": 5,
|
|
||||||
"label": "Open in GitHub",
|
|
||||||
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -49,7 +42,7 @@ jobs:
|
||||||
run: yarn build-types
|
run: yarn build-types
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: yarn tsx scripts/deploy.ts
|
run: yarn tsx scripts/deploy-dotcom.ts
|
||||||
env:
|
env:
|
||||||
RELEASE_COMMIT_HASH: ${{ github.sha }}
|
RELEASE_COMMIT_HASH: ${{ github.sha }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
2
apps/bemo-worker/.gitignore
vendored
Normal file
2
apps/bemo-worker/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
build
|
||||||
|
.wrangler
|
38
apps/bemo-worker/README.md
Normal file
38
apps/bemo-worker/README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# bemo-worker
|
||||||
|
|
||||||
|
```
|
||||||
|
⠀⠀⠀⠀⠀⠀⢤⡴⠶⠷⠶⠶⠾⠷⠻⠶⠷⠿⠾⠶⠷⠿⠳⠷⠾⠾⠷⠷⠾⠾⠷⠿⠶⠿⠞⠷⠶⠷⢦⣤⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢠⡼⠟⠋⠤⣤⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠻⣤⡤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡧⠀⠁⡀⢀⠉⠙⠠⣄⣀⠀⠀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣘⣛⣧⣤⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡇⠐⠠⠀⣠⣤⡘⠀⠌⡈⣷⡶⠛⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠙⠉⠋⠛⠋⣶⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡧⢐⣶⣶⠙⠋⠃⣴⣮⠁⣿⠀⠀⠀⠀⢠⣄⣤⣠⣄⣤⣤⣤⣠⣤⣤⣤⣠⣤⣤⣤⣠⣤⣤⣤⣄⣤⣠⣄⣠⡄⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡗⢸⠏⢁⡶⣶⡶⠏⠉⡀⣿⠀⠀⠐⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠐⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡧⠰⣿⡾⠁⠉⠀⣾⣿⠄⣿⠀⠀⠂⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡧⠐⠉⡁⣰⣶⡆⠉⠉⠀⣿⠀⠀⡀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠠⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡇⢈⠐⠀⠀⠉⡀⢀⠁⠂⣿⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠛⣤⡼⠋⠀⠀⠀⠀⠀⠀⠀⠀⠛⢣⣤⠛⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⣏⢠⣷⣾⣾⣤⣠⡀⠂⡁⣿⠀⠀⠈⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠶⣆⣀⣀⣰⠶⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⢀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡇⢸⣿⣿⣿⢿⣿⡃⠄⡀⣿⠀⠀⠐⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡇⠸⣟⣿⣻⣿⣿⡄⠀⠄⣿⠀⠀⠠⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⣤⣿⣿⣤
|
||||||
|
⠀⠀⠀⠀⢸⡗⠰⣿⣿⣿⣿⣾⠂⠄⠂⣿⠀⠀⢀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠈⣿⠀⠀⠀⠀⠀⠀⣿⠀⢸⣯
|
||||||
|
⠀⠀⠀⠀⢸⡏⠘⠉⣿⣿⣿⡿⠆⠀⡁⣿⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣿⠀⠀⠀⠀⠀⠀⣿⠀⢸⣧
|
||||||
|
⠀⠀⠀⠀⢸⣇⠨⣿⣿⣿⣿⣿⡁⠂⠄⣿⠀⠀⠂⠸⢧⣠⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⡼⠇⠀⠈⣿⠀⠀⠀⠀⠀⠀⣿⠀⢸⡿
|
||||||
|
⠀⠀⠀⠀⢸⡧⠘⠉⠙⠿⠻⠛⠂⢀⠂⣿⠀⠀⢀⠀⠀⢁⢈⡈⣁⢈⡁⣈⢁⡈⢁⠈⠁⠁⠈⠈⠀⠁⠈⠁⠉⠈⠁⠈⠀⠁⠀⠀⠠⣿⣤⠀⠀⠀⠀⠀⣿⡀⣸⣿
|
||||||
|
⠀⠀⠀⠀⢸⣇⣠⣢⣷⣶⣶⣦⡑⠀⠄⣿⠀⠀⠀⢰⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⠀⠀⠠⠀⠀⢀⣶⣶⠀⠀⠄⠐⠀⠐⣿⣼⠟⢣⣤⣤⠟⠁⣰⠿⠀
|
||||||
|
⠀⣀⣰⣶⠾⠛⠛⠙⠛⠉⣿⣿⡀⠐⡀⣿⠀⠀⠄⠘⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠉⠀⠀⢀⠠⠀⠘⠛⠛⠁⠀⠀⠀⠄⠈⣿⠙⠶⢧⣀⣀⣰⡾⠋⠀⠀
|
||||||
|
⣶⠋⠉⣘⣟⢿⣻⣟⡻⠻⠿⣿⠀⠄⡀⣿⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠈⠀⠀⠀⡀⠀⠀⠀⡀⠐⠀⢀⠀⣿⠀⠀⠈⠉⠉⠉⠁⠀⠀⠀
|
||||||
|
⠛⣦⣤⣤⣤⣤⣤⣤⣤⣤⠀⢹⣟⠀⡀⣿⠀⠀⠈⢀⣀⣻⡏⠉⣇⣀⡐⠀⠀⠄⠀⠀⠂⠀⢀⠀⣠⡼⠧⣄⠀⣠⣦⠄⠀⢀⠠⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡇⠠⠐⠠⠀⠛⠛⡀⠄⡀⣿⠀⠀⠠⢼⣇⣀⡀⢀⣀⣸⠇⠀⠀⠀⠀⠂⠀⠠⠀⠘⠛⠒⠒⠚⠻⣿⣂⠼⡇⠀⠀⢀⠠⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⢸⡇⠠⠁⠂⠡⠈⡀⠄⠂⠄⣿⠀⠀⠀⠀⠀⠹⢇⣠⡏⠀⠀⠀⠀⠁⠀⡀⠐⠀⠀⠀⣠⣶⣿⣿⣷⣬⡉⠁⠀⠀⢀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠸⢷⣄⠂⣁⠂⠡⢀⠐⡈⠀⣿⠀⠀⠈⠀⠀⢂⣀⡠⠀⠀⠐⠀⠁⡀⠄⠀⠀⠀⠐⠀⣿⣿⣿⣿⣷⣿⡇⠀⠀⠠⠀⠀⠠⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠘⢳⣤⡄⢁⠂⠐⡀⠁⣿⠀⠀⢀⠘⠛⠛⠛⠃⠀⠘⠛⠛⠛⠛⠀⠀⠀⠁⠀⠀⠈⠻⠿⠿⠿⠋⠀⠀⢀⠀⠀⠐⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠛⡶⢦⣀⣄⣿⠀⠀⡀⠀⠠⠀⠀⠀⡀⠀⢀⠀⢀⠀⠀⡀⠂⠀⠈⠀⡀⠀⠀⠀⠀⠀⠀⠄⠀⠀⠈⠀⢀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠘⠛⢻⣿⣤⡀⣁⣀⡀⣀⢂⣀⣁⣀⣈⡀⣈⣀⣀⣁⣀⡐⣀⣀⢁⡀⣂⢀⣀⢂⣀⣀⢀⢂⣀⣠⣼⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠛⠛⠛⠛⢻⣿⣻⣿⣛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⣻⣟⣿⣿⣿⡛⠛⠛⠛⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⡿⣿⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣻⣿⣿⣿⣯⣷⡗⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⣿⢻⣧⠀⠀⠀⠀⠀⠀⠀⠀⢰⡞⣿⣿⡶⣯⣽⡿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣏⣿⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣶⡟⢩⣟⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠿⣧⣾⣿⣶⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||||
|
```
|
49
apps/bemo-worker/package.json
Normal file
49
apps/bemo-worker/package.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "@tldraw/bemo-worker",
|
||||||
|
"description": "A tiny little drawing app (merge server).",
|
||||||
|
"version": "2.0.0-alpha.11",
|
||||||
|
"private": true,
|
||||||
|
"author": {
|
||||||
|
"name": "tldraw GB Ltd.",
|
||||||
|
"email": "hello@tldraw.com"
|
||||||
|
},
|
||||||
|
"main": "./src/worker.ts",
|
||||||
|
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
|
||||||
|
"files": [],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "yarn run -T tsx ../../scripts/workers/dev.ts",
|
||||||
|
"test-ci": "lazy inherit",
|
||||||
|
"test": "yarn run -T jest",
|
||||||
|
"test-coverage": "lazy inherit",
|
||||||
|
"check-bundle-size": "yarn run -T tsx ../../scripts/check-worker-bundle.ts --entry src/worker.ts --size-limit-bytes 350000",
|
||||||
|
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tldraw/dotcom-shared": "workspace:*",
|
||||||
|
"@tldraw/store": "workspace:*",
|
||||||
|
"@tldraw/tlschema": "workspace:*",
|
||||||
|
"@tldraw/tlsync": "workspace:*",
|
||||||
|
"@tldraw/utils": "workspace:*",
|
||||||
|
"itty-router": "^4.0.13",
|
||||||
|
"nanoid": "4.0.2",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"toucan-js": "^3.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
|
"esbuild": "^0.21.5",
|
||||||
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"wrangler": "3.62.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "config/jest/node",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^~(.*)": "<rootDir>/src/$1"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!(nanoid|escape-string-regexp)/)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
38
apps/bemo-worker/src/BemoDO.ts
Normal file
38
apps/bemo-worker/src/BemoDO.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { DurableObject } from 'cloudflare:workers'
|
||||||
|
import { Router } from 'itty-router'
|
||||||
|
import { createSentry } from './sentry'
|
||||||
|
import { Environment } from './types'
|
||||||
|
|
||||||
|
export class BemoDO extends DurableObject<Environment> {
|
||||||
|
private reportError(e: unknown, request?: Request) {
|
||||||
|
const sentry = createSentry(this.ctx, this.env, request)
|
||||||
|
console.error(e)
|
||||||
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
|
sentry.captureException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly router = Router()
|
||||||
|
.get('/do', async () => {
|
||||||
|
return Response.json({ message: 'Hello from a durable object!' })
|
||||||
|
})
|
||||||
|
.get('/do/error', async () => {
|
||||||
|
this.doAnError()
|
||||||
|
})
|
||||||
|
.all('*', async () => new Response('Not found', { status: 404 }))
|
||||||
|
|
||||||
|
private doAnError() {
|
||||||
|
throw new Error('this is an error from a DO')
|
||||||
|
}
|
||||||
|
|
||||||
|
override async fetch(request: Request<unknown, CfProperties<unknown>>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return await this.router.handle(request)
|
||||||
|
} catch (error) {
|
||||||
|
this.reportError(error, request)
|
||||||
|
return new Response('Something went wrong', {
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
apps/bemo-worker/src/sentry.ts
Normal file
21
apps/bemo-worker/src/sentry.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Toucan } from 'toucan-js'
|
||||||
|
import { Environment } from './types'
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
waitUntil: ExecutionContext['waitUntil']
|
||||||
|
request?: Request
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSentry(ctx: Context, env: Environment, request?: Request) {
|
||||||
|
return new Toucan({
|
||||||
|
dsn: env.SENTRY_DSN,
|
||||||
|
release: `${env.WORKER_NAME}.${env.CF_VERSION_METADATA.id}`,
|
||||||
|
environment: env.WORKER_NAME,
|
||||||
|
context: ctx,
|
||||||
|
request,
|
||||||
|
requestDataOptions: {
|
||||||
|
allowedHeaders: ['user-agent'],
|
||||||
|
allowedSearchParams: /(.*)/,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
12
apps/bemo-worker/src/types.ts
Normal file
12
apps/bemo-worker/src/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { BemoDO } from './BemoDO'
|
||||||
|
|
||||||
|
export interface Environment {
|
||||||
|
// bindings
|
||||||
|
BEMO_DO: DurableObjectNamespace<BemoDO>
|
||||||
|
|
||||||
|
TLDRAW_ENV: string | undefined
|
||||||
|
SENTRY_DSN: string | undefined
|
||||||
|
IS_LOCAL: string | undefined
|
||||||
|
WORKER_NAME: string | undefined
|
||||||
|
CF_VERSION_METADATA: WorkerVersionMetadata
|
||||||
|
}
|
5
apps/bemo-worker/src/worker.test.ts
Normal file
5
apps/bemo-worker/src/worker.test.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
describe('worker', () => {
|
||||||
|
it('works', () => {
|
||||||
|
// blank test to make ci happy
|
||||||
|
})
|
||||||
|
})
|
37
apps/bemo-worker/src/worker.ts
Normal file
37
apps/bemo-worker/src/worker.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/// <reference no-default-lib="true"/>
|
||||||
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
|
import { WorkerEntrypoint } from 'cloudflare:workers'
|
||||||
|
import { Router, createCors } from 'itty-router'
|
||||||
|
import { createSentry } from './sentry'
|
||||||
|
import { Environment } from './types'
|
||||||
|
|
||||||
|
export { BemoDO } from './BemoDO'
|
||||||
|
|
||||||
|
const cors = createCors({ origins: ['*'] })
|
||||||
|
|
||||||
|
export default class Worker extends WorkerEntrypoint<Environment> {
|
||||||
|
private readonly router = Router()
|
||||||
|
.all('*', cors.preflight)
|
||||||
|
.get('/do', async (request) => {
|
||||||
|
const bemo = this.env.BEMO_DO.get(this.env.BEMO_DO.idFromName('bemo-do'))
|
||||||
|
const message = await (await bemo.fetch(request)).json()
|
||||||
|
return Response.json(message)
|
||||||
|
})
|
||||||
|
.all('*', async () => new Response('Not found', { status: 404 }))
|
||||||
|
|
||||||
|
override async fetch(request: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return await this.router.handle(request).then(cors.corsify)
|
||||||
|
} catch (error) {
|
||||||
|
const sentry = createSentry(this.ctx, this.env, request)
|
||||||
|
console.error(error)
|
||||||
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
|
sentry.captureException(error)
|
||||||
|
return new Response('Something went wrong', {
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
apps/bemo-worker/tsconfig.json
Normal file
26
apps/bemo-worker/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"extends": "../../config/tsconfig.base.json",
|
||||||
|
"include": ["src", "scripts"],
|
||||||
|
"exclude": ["node_modules", "dist", ".tsbuild*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"emitDeclarationOnly": false
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../packages/dotcom-shared"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/store"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/tlschema"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/tlsync"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/utils"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
79
apps/bemo-worker/wrangler.toml
Normal file
79
apps/bemo-worker/wrangler.toml
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
main = "src/worker.ts"
|
||||||
|
compatibility_date = "2024-06-25"
|
||||||
|
upload_source_maps = true
|
||||||
|
|
||||||
|
[dev]
|
||||||
|
port = 8989
|
||||||
|
ip = "0.0.0.0"
|
||||||
|
|
||||||
|
# these migrations are append-only. you can't change them. if you do need to change something, do so
|
||||||
|
# by creating new migrations
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v1" # Should be unique for each entry
|
||||||
|
new_classes = ["BemoDO"]
|
||||||
|
|
||||||
|
|
||||||
|
#################### Environment names ####################
|
||||||
|
# dev should never actually get deployed anywhere
|
||||||
|
[env.dev]
|
||||||
|
name = "dev-bemo"
|
||||||
|
|
||||||
|
# we don't have a hard-coded name for preview. we instead have to generate it at build time and append it to this file.
|
||||||
|
|
||||||
|
# staging is the same as a preview on main:
|
||||||
|
[env.staging]
|
||||||
|
name = "canary-bemo"
|
||||||
|
routes = [
|
||||||
|
{ pattern = "canary-demo.tldraw.xyz", custom_domain = true }
|
||||||
|
]
|
||||||
|
|
||||||
|
# production gets the proper name
|
||||||
|
[env.production]
|
||||||
|
name = "production-bemo"
|
||||||
|
routes = [
|
||||||
|
{ pattern = "demo.tldraw.xyz", custom_domain = true }
|
||||||
|
]
|
||||||
|
|
||||||
|
#################### Durable objects ####################
|
||||||
|
# durable objects have the same configuration in all environments:
|
||||||
|
|
||||||
|
[durable_objects]
|
||||||
|
bindings = [
|
||||||
|
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[env.dev.durable_objects]
|
||||||
|
bindings = [
|
||||||
|
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[env.preview.durable_objects]
|
||||||
|
bindings = [
|
||||||
|
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[env.staging.durable_objects]
|
||||||
|
bindings = [
|
||||||
|
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[env.production.durable_objects]
|
||||||
|
bindings = [
|
||||||
|
{ name = "BEMO_DO", class_name = "BemoDO" },
|
||||||
|
]
|
||||||
|
|
||||||
|
#################### Version metadata ####################
|
||||||
|
[version_metadata]
|
||||||
|
binding = "CF_VERSION_METADATA"
|
||||||
|
|
||||||
|
[env.dev.version_metadata]
|
||||||
|
binding = "CF_VERSION_METADATA"
|
||||||
|
|
||||||
|
[env.preview.version_metadata]
|
||||||
|
binding = "CF_VERSION_METADATA"
|
||||||
|
|
||||||
|
[env.staging.version_metadata]
|
||||||
|
binding = "CF_VERSION_METADATA"
|
||||||
|
|
||||||
|
[env.production.version_metadata]
|
||||||
|
binding = "CF_VERSION_METADATA"
|
|
@ -23,7 +23,7 @@
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
"@types/ws": "^8.5.9",
|
"@types/ws": "^8.5.9",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
"wrangler": "3.61.0"
|
"wrangler": "3.62.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "config/jest/node",
|
"preset": "config/jest/node",
|
||||||
|
|
|
@ -26,6 +26,9 @@ preview_bucket_name = 'uploads-preview'
|
||||||
type = "analytics_engine"
|
type = "analytics_engine"
|
||||||
name = "MEASURE"
|
name = "MEASURE"
|
||||||
|
|
||||||
|
[[env.staging.analytics_engine_datasets]]
|
||||||
|
binding = "MEASURE"
|
||||||
|
|
||||||
|
|
||||||
# production settings
|
# production settings
|
||||||
[env.production]
|
[env.production]
|
||||||
|
@ -45,7 +48,13 @@ preview_bucket_name = 'uploads-preview'
|
||||||
type = "analytics_engine"
|
type = "analytics_engine"
|
||||||
name = "MEASURE"
|
name = "MEASURE"
|
||||||
|
|
||||||
|
[[env.production.analytics_engine_datasets]]
|
||||||
|
binding = "MEASURE"
|
||||||
|
|
||||||
[[env.preview.r2_buckets]]
|
[[env.preview.r2_buckets]]
|
||||||
binding = 'UPLOADS'
|
binding = 'UPLOADS'
|
||||||
bucket_name = 'uploads'
|
bucket_name = 'uploads'
|
||||||
preview_bucket_name = 'uploads-preview'
|
preview_bucket_name = 'uploads-preview'
|
||||||
|
|
||||||
|
[[env.preview.analytics_engine_datasets]]
|
||||||
|
binding = "MEASURE"
|
|
@ -7,18 +7,15 @@
|
||||||
"name": "tldraw GB Ltd.",
|
"name": "tldraw GB Ltd.",
|
||||||
"email": "hello@tldraw.com"
|
"email": "hello@tldraw.com"
|
||||||
},
|
},
|
||||||
"main": "./src/lib/worker.ts",
|
"main": "./src/worker.ts",
|
||||||
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
|
"/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here",
|
||||||
"files": [],
|
"files": [],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently --kill-others yarn:dev-cron yarn:dev-wrangler yarn:report-size",
|
"dev": "yarn run -T tsx ../../scripts/workers/dev.ts",
|
||||||
"dev-cron": "yarn run -T tsx ./scripts/cron.ts",
|
|
||||||
"dev-wrangler": "yarn run -T tsx ./scripts/dev-wrap.ts",
|
|
||||||
"report-size": "node scripts/report-size.js",
|
|
||||||
"test-ci": "lazy inherit",
|
"test-ci": "lazy inherit",
|
||||||
"test": "yarn run -T jest",
|
"test": "yarn run -T jest",
|
||||||
"test-coverage": "lazy inherit",
|
"test-coverage": "lazy inherit",
|
||||||
"check-bundle-size": "yarn run -T tsx ../../scripts/check-worker-bundle.ts --entry src/lib/worker.ts --size-limit-bytes 350000",
|
"check-bundle-size": "yarn run -T tsx ../../scripts/check-worker-bundle.ts --entry src/worker.ts --size-limit-bytes 350000",
|
||||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -29,21 +26,18 @@
|
||||||
"@tldraw/tlschema": "workspace:*",
|
"@tldraw/tlschema": "workspace:*",
|
||||||
"@tldraw/tlsync": "workspace:*",
|
"@tldraw/tlsync": "workspace:*",
|
||||||
"@tldraw/utils": "workspace:*",
|
"@tldraw/utils": "workspace:*",
|
||||||
"esbuild": "^0.18.4",
|
|
||||||
"itty-router": "^4.0.13",
|
"itty-router": "^4.0.13",
|
||||||
"nanoid": "4.0.2",
|
"nanoid": "4.0.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"toucan-js": "^3.4.0"
|
||||||
"toucan-js": "^2.7.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
"concurrently": "^8.2.2",
|
"esbuild": "^0.21.5",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
"picocolors": "^1.0.0",
|
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"wrangler": "3.61.0"
|
"wrangler": "3.62.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "config/jest/node",
|
"preset": "config/jest/node",
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
const CRON_INTERVAL_MS = 10_000
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
try {
|
|
||||||
await fetch('http://127.0.0.1:8787/__scheduled')
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Error triggering cron:', err)
|
|
||||||
}
|
|
||||||
}, CRON_INTERVAL_MS)
|
|
|
@ -1,45 +0,0 @@
|
||||||
/* eslint-disable no-undef */
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
const { spawn } = require('child_process')
|
|
||||||
const colors = require('picocolors')
|
|
||||||
|
|
||||||
class Monitor {
|
|
||||||
lastLineTime = Date.now()
|
|
||||||
nextTick = 0
|
|
||||||
|
|
||||||
size = 0
|
|
||||||
|
|
||||||
start() {
|
|
||||||
console.log('Spawning')
|
|
||||||
const proc = spawn('npx', ['esbuild', 'src/lib/worker.ts', '--bundle', '--minify', '--watch'])
|
|
||||||
// listen for lines on stdin
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
this.size += data.length
|
|
||||||
this.lastLineTime = Date.now()
|
|
||||||
clearTimeout(this.nextTick)
|
|
||||||
this.nextTick = setTimeout(() => {
|
|
||||||
console.log(
|
|
||||||
colors.bold(colors.yellow('dotcom-worker')),
|
|
||||||
'is roughly',
|
|
||||||
colors.bold(colors.cyan(Math.floor(this.size / 1024) + 'kb')),
|
|
||||||
'(minified)\n'
|
|
||||||
)
|
|
||||||
this.size = 0
|
|
||||||
}, 10)
|
|
||||||
})
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('Int')
|
|
||||||
proc.kill()
|
|
||||||
})
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
console.log('Term')
|
|
||||||
proc.kill()
|
|
||||||
})
|
|
||||||
process.on('exit', () => {
|
|
||||||
console.log('Exiting')
|
|
||||||
proc.kill()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new Monitor().start()
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from '@tldraw/tlsync'
|
} from '@tldraw/tlsync'
|
||||||
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
|
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
|
||||||
import { IRequest, Router } from 'itty-router'
|
import { IRequest, Router } from 'itty-router'
|
||||||
import Toucan from 'toucan-js'
|
import { Toucan } from 'toucan-js'
|
||||||
import { AlarmScheduler } from './AlarmScheduler'
|
import { AlarmScheduler } from './AlarmScheduler'
|
||||||
import { PERSIST_INTERVAL_MS } from './config'
|
import { PERSIST_INTERVAL_MS } from './config'
|
||||||
import { getR2KeyForRoom } from './r2'
|
import { getR2KeyForRoom } from './r2'
|
||||||
|
@ -218,21 +218,17 @@ export class TLDrawDurableObject {
|
||||||
const sentry = new Toucan({
|
const sentry = new Toucan({
|
||||||
dsn: this.sentryDSN,
|
dsn: this.sentryDSN,
|
||||||
request: req,
|
request: req,
|
||||||
|
requestDataOptions: {
|
||||||
allowedHeaders: ['user-agent'],
|
allowedHeaders: ['user-agent'],
|
||||||
allowedSearchParams: /(.*)/,
|
allowedSearchParams: /(.*)/,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.router.handle(req).catch((err) => {
|
return await this.router.handle(req)
|
||||||
console.error(err)
|
|
||||||
sentry.captureException(err)
|
|
||||||
|
|
||||||
return new Response('Something went wrong', {
|
|
||||||
status: 500,
|
|
||||||
statusText: 'Internal Server Error',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
sentry.captureException(err)
|
sentry.captureException(err)
|
||||||
return new Response('Something went wrong', {
|
return new Response('Something went wrong', {
|
||||||
status: 500,
|
status: 500,
|
|
@ -7,7 +7,7 @@ import {
|
||||||
ROOM_PREFIX,
|
ROOM_PREFIX,
|
||||||
} from '@tldraw/dotcom-shared'
|
} from '@tldraw/dotcom-shared'
|
||||||
import { Router, createCors } from 'itty-router'
|
import { Router, createCors } from 'itty-router'
|
||||||
import Toucan from 'toucan-js'
|
import { Toucan } from 'toucan-js'
|
||||||
import { createRoom } from './routes/createRoom'
|
import { createRoom } from './routes/createRoom'
|
||||||
import { createRoomSnapshot } from './routes/createRoomSnapshot'
|
import { createRoomSnapshot } from './routes/createRoomSnapshot'
|
||||||
import { forwardRoomRequest } from './routes/forwardRoomRequest'
|
import { forwardRoomRequest } from './routes/forwardRoomRequest'
|
||||||
|
@ -51,14 +51,17 @@ const Worker = {
|
||||||
dsn: env.SENTRY_DSN,
|
dsn: env.SENTRY_DSN,
|
||||||
context, // Includes 'waitUntil', which is essential for Sentry logs to be delivered. Modules workers do not include 'request' in context -- you'll need to set it separately.
|
context, // Includes 'waitUntil', which is essential for Sentry logs to be delivered. Modules workers do not include 'request' in context -- you'll need to set it separately.
|
||||||
request, // request is not included in 'context', so we set it here.
|
request, // request is not included in 'context', so we set it here.
|
||||||
|
requestDataOptions: {
|
||||||
allowedHeaders: ['user-agent'],
|
allowedHeaders: ['user-agent'],
|
||||||
allowedSearchParams: /(.*)/,
|
allowedSearchParams: /(.*)/,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
.handle(request, env, context)
|
.handle(request, env, context)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
sentry.captureException(err)
|
sentry.captureException(err)
|
||||||
|
|
||||||
return new Response('Something went wrong', {
|
return new Response('Something went wrong', {
|
|
@ -1,4 +1,4 @@
|
||||||
main = "src/lib/worker.ts"
|
main = "src/worker.ts"
|
||||||
compatibility_date = "2024-06-19"
|
compatibility_date = "2024-06-19"
|
||||||
|
|
||||||
[dev]
|
[dev]
|
||||||
|
|
|
@ -22,6 +22,7 @@ Sentry.init({
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
release: sentryReleaseName,
|
release: sentryReleaseName,
|
||||||
environment: env,
|
environment: env,
|
||||||
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
integrations: [new ExtraErrorData({ depth: 10 }) as any],
|
integrations: [new ExtraErrorData({ depth: 10 }) as any],
|
||||||
// ...
|
// ...
|
||||||
// Note: if you want to override the automatic release value, do not set a
|
// Note: if you want to override the automatic release value, do not set a
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
"@types/node": "~20.11",
|
"@types/node": "~20.11",
|
||||||
"discord-api-types": "^0.37.67",
|
"discord-api-types": "^0.37.67",
|
||||||
"esbuild": "^0.18.4",
|
"esbuild": "^0.21.5",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"wrangler": "3.61.0"
|
"wrangler": "3.62.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"create-serve": "1.0.1",
|
"create-serve": "1.0.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"esbuild": "^0.18.4",
|
"esbuild": "^0.21.5",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
|
@ -137,7 +137,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||||
"@typescript-eslint/parser": "^5.57.0",
|
"@typescript-eslint/parser": "^5.57.0",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"esbuild": "^0.18.4",
|
"esbuild": "^0.21.5",
|
||||||
"eslint": "^8.37.0",
|
"eslint": "^8.37.0",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
"clean": "scripts/clean.sh",
|
"clean": "scripts/clean.sh",
|
||||||
"postinstall": "husky install && yarn refresh-assets",
|
"postinstall": "husky install && yarn refresh-assets",
|
||||||
"refresh-assets": "lazy refresh-assets",
|
"refresh-assets": "lazy refresh-assets",
|
||||||
"dev": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/examples' --filter='packages/tldraw'",
|
"dev": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/examples' --filter='packages/tldraw' --filter='apps/bemo-worker'",
|
||||||
"dev-vscode": "code ./apps/vscode/extension && lazy run dev --filter='apps/vscode/{extension,editor}'",
|
"dev-vscode": "code ./apps/vscode/extension && lazy run dev --filter='apps/vscode/{extension,editor}'",
|
||||||
"dev-app": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/{dotcom,dotcom-asset-upload,dotcom-worker}' --filter='packages/tldraw'",
|
"dev-app": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/{dotcom,dotcom-asset-upload,dotcom-worker}' --filter='packages/tldraw'",
|
||||||
"dev-docs": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/docs'",
|
"dev-docs": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/docs'",
|
||||||
|
@ -120,6 +120,7 @@
|
||||||
"@sentry/cli": "^2.25.0",
|
"@sentry/cli": "^2.25.0",
|
||||||
"@yarnpkg/types": "^4.0.0",
|
"@yarnpkg/types": "^4.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"esbuild": "^0.21.5",
|
||||||
"mime": "^4.0.3",
|
"mime": "^4.0.3",
|
||||||
"purgecss": "^5.0.0",
|
"purgecss": "^5.0.0",
|
||||||
"svgo": "^3.0.2"
|
"svgo": "^3.0.2"
|
||||||
|
|
|
@ -41,7 +41,7 @@ export const assert: (value: unknown, message?: string) => asserts value = omitF
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const assertExists = omitFromStackTrace(<T>(value: T, message?: string): NonNullable<T> => {
|
export const assertExists = omitFromStackTrace(<T>(value: T, message?: string): NonNullable<T> => {
|
||||||
// note that value == null is equivilent to value === null || value === undefined
|
// note that value == null is equivalent to value === null || value === undefined
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
throw new Error(message ?? 'value must be defined')
|
throw new Error(message ?? 'value must be defined')
|
||||||
}
|
}
|
||||||
|
|
120
scripts/deploy-bemo.ts
Normal file
120
scripts/deploy-bemo.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import assert from 'assert'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import path, { join } from 'path'
|
||||||
|
import toml from 'toml'
|
||||||
|
import {
|
||||||
|
createGithubDeployment,
|
||||||
|
getDeployInfo,
|
||||||
|
setWranglerPreviewConfig,
|
||||||
|
wranglerDeploy,
|
||||||
|
} from './lib/deploy'
|
||||||
|
import { Discord } from './lib/discord'
|
||||||
|
import { exec } from './lib/exec'
|
||||||
|
import { makeEnv } from './lib/makeEnv'
|
||||||
|
import { nicelog } from './lib/nicelog'
|
||||||
|
|
||||||
|
const workerDir = path.relative(process.cwd(), path.resolve(__dirname, '../apps/bemo-worker'))
|
||||||
|
|
||||||
|
// Do not use `process.env` directly in this script. Add your variable to `makeEnv` and use it via
|
||||||
|
// `env` instead. This makes sure that all required env vars are present.
|
||||||
|
const env = makeEnv([
|
||||||
|
'CLOUDFLARE_ACCOUNT_ID',
|
||||||
|
'CLOUDFLARE_API_TOKEN',
|
||||||
|
'DISCORD_DEPLOY_WEBHOOK_URL',
|
||||||
|
'RELEASE_COMMIT_HASH',
|
||||||
|
'TLDRAW_ENV',
|
||||||
|
'GH_TOKEN',
|
||||||
|
'SENTRY_AUTH_TOKEN',
|
||||||
|
'SENTRY_BEMO_WORKER_DSN',
|
||||||
|
])
|
||||||
|
|
||||||
|
const discord = new Discord({
|
||||||
|
webhookUrl: env.DISCORD_DEPLOY_WEBHOOK_URL,
|
||||||
|
shouldNotify: env.TLDRAW_ENV === 'production',
|
||||||
|
totalSteps: 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { previewId, sha } = getDeployInfo()
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
assert(
|
||||||
|
env.TLDRAW_ENV === 'staging' || env.TLDRAW_ENV === 'production' || env.TLDRAW_ENV === 'preview',
|
||||||
|
'TLDRAW_ENV must be staging or production or preview'
|
||||||
|
)
|
||||||
|
|
||||||
|
await discord.message(`--- **${env.TLDRAW_ENV} bemo deploy pre-flight** ---`)
|
||||||
|
|
||||||
|
await discord.step('setting up deploy', async () => {
|
||||||
|
await exec('yarn', ['lazy', 'prebuild'])
|
||||||
|
})
|
||||||
|
|
||||||
|
await discord.step('cloudflare deploy dry run', async () => {
|
||||||
|
await deployBemoWorker({ dryRun: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- point of no return! do the deploy for real --- //
|
||||||
|
|
||||||
|
await discord.message(`--- **pre-flight complete, starting real bemo deploy** ---`)
|
||||||
|
|
||||||
|
// 2. deploy the cloudflare workers:
|
||||||
|
await discord.step('deploying bemo-worker to cloudflare', async () => {
|
||||||
|
await deployBemoWorker({ dryRun: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
// we set the domain in the wrangler.toml file since it's managed by cloudflare
|
||||||
|
const domain = toml.parse(readFileSync(join(workerDir, 'wrangler.toml')).toString())?.env[
|
||||||
|
env.TLDRAW_ENV
|
||||||
|
]?.routes?.[0]?.pattern
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error('Could not find the domain in wrangler.toml')
|
||||||
|
}
|
||||||
|
|
||||||
|
const deploymentUrl = `https://${domain}`
|
||||||
|
|
||||||
|
nicelog('Creating deployment for', deploymentUrl)
|
||||||
|
await createGithubDeployment(env, {
|
||||||
|
app: 'bemo',
|
||||||
|
deploymentUrl,
|
||||||
|
sha,
|
||||||
|
})
|
||||||
|
|
||||||
|
await discord.message(`**Deploy complete!**`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let didUpdateBemoWorker = false
|
||||||
|
async function deployBemoWorker({ dryRun }: { dryRun: boolean }) {
|
||||||
|
const workerId = `${previewId ?? env.TLDRAW_ENV}-bemo`
|
||||||
|
if (previewId && !didUpdateBemoWorker) {
|
||||||
|
await setWranglerPreviewConfig(workerDir, {
|
||||||
|
name: workerId,
|
||||||
|
customDomain: `${previewId}-demo.tldraw.xyz`,
|
||||||
|
})
|
||||||
|
didUpdateBemoWorker = true
|
||||||
|
}
|
||||||
|
|
||||||
|
await wranglerDeploy({
|
||||||
|
location: workerDir,
|
||||||
|
dryRun,
|
||||||
|
env: env.TLDRAW_ENV,
|
||||||
|
vars: {
|
||||||
|
WORKER_NAME: workerId,
|
||||||
|
TLDRAW_ENV: env.TLDRAW_ENV,
|
||||||
|
SENTRY_DSN: env.SENTRY_BEMO_WORKER_DSN,
|
||||||
|
},
|
||||||
|
sentry: {
|
||||||
|
authToken: env.SENTRY_AUTH_TOKEN,
|
||||||
|
project: 'bemo-worker',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(async (err) => {
|
||||||
|
// don't notify discord on preview builds
|
||||||
|
if (env.TLDRAW_ENV !== 'preview') {
|
||||||
|
await discord.message(`${Discord.AT_TEAM_MENTION} Deploy failed: ${err.stack}`, {
|
||||||
|
always: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
|
@ -1,12 +1,18 @@
|
||||||
import * as github from '@actions/github'
|
|
||||||
import { GetObjectCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
|
import { GetObjectCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
|
||||||
import { Upload } from '@aws-sdk/lib-storage'
|
import { Upload } from '@aws-sdk/lib-storage'
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
import { appendFileSync, existsSync, readdirSync, writeFileSync } from 'fs'
|
import { existsSync, readdirSync, writeFileSync } from 'fs'
|
||||||
import path, { join } from 'path'
|
import path from 'path'
|
||||||
import { PassThrough } from 'stream'
|
import { PassThrough } from 'stream'
|
||||||
import * as tar from 'tar'
|
import * as tar from 'tar'
|
||||||
|
import {
|
||||||
|
createGithubDeployment,
|
||||||
|
getDeployInfo,
|
||||||
|
setWranglerPreviewConfig,
|
||||||
|
wranglerDeploy,
|
||||||
|
} from './lib/deploy'
|
||||||
|
import { Discord } from './lib/discord'
|
||||||
import { exec } from './lib/exec'
|
import { exec } from './lib/exec'
|
||||||
import { makeEnv } from './lib/makeEnv'
|
import { makeEnv } from './lib/makeEnv'
|
||||||
import { nicelog } from './lib/nicelog'
|
import { nicelog } from './lib/nicelog'
|
||||||
|
@ -48,26 +54,13 @@ const env = makeEnv([
|
||||||
'R2_ACCESS_KEY_SECRET',
|
'R2_ACCESS_KEY_SECRET',
|
||||||
])
|
])
|
||||||
|
|
||||||
const githubPrNumber = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/)?.[1]
|
const discord = new Discord({
|
||||||
function getPreviewId() {
|
webhookUrl: env.DISCORD_DEPLOY_WEBHOOK_URL,
|
||||||
if (env.TLDRAW_ENV !== 'preview') return undefined
|
shouldNotify: env.TLDRAW_ENV === 'production',
|
||||||
if (githubPrNumber) return `pr-${githubPrNumber}`
|
totalSteps: 8,
|
||||||
return process.env.TLDRAW_PREVIEW_ID ?? undefined
|
})
|
||||||
}
|
|
||||||
const previewId = getPreviewId()
|
|
||||||
|
|
||||||
if (env.TLDRAW_ENV === 'preview' && !previewId) {
|
|
||||||
throw new Error(
|
|
||||||
'If running preview deploys from outside of a PR action, TLDRAW_PREVIEW_ID env var must be set'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const sha =
|
|
||||||
// if the event is 'pull_request', github.context.sha is an ephemeral merge commit
|
|
||||||
// while the actual commit we want to create the deployment for is the 'head' of the PR.
|
|
||||||
github.context.eventName === 'pull_request'
|
|
||||||
? github.context.payload.pull_request?.head.sha
|
|
||||||
: github.context.sha
|
|
||||||
|
|
||||||
|
const { previewId, sha } = getDeployInfo()
|
||||||
const sentryReleaseName = `${env.TLDRAW_ENV}-${previewId ? previewId + '-' : ''}-${sha}`
|
const sentryReleaseName = `${env.TLDRAW_ENV}-${previewId ? previewId + '-' : ''}-${sha}`
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
@ -76,9 +69,9 @@ async function main() {
|
||||||
'TLDRAW_ENV must be staging or production or preview'
|
'TLDRAW_ENV must be staging or production or preview'
|
||||||
)
|
)
|
||||||
|
|
||||||
await discordMessage(`--- **${env.TLDRAW_ENV} deploy pre-flight** ---`)
|
await discord.message(`--- **${env.TLDRAW_ENV} dotcom deploy pre-flight** ---`)
|
||||||
|
|
||||||
await discordStep('[1/7] setting up deploy', async () => {
|
await discord.step('setting up deploy', async () => {
|
||||||
// make sure the tldraw .css files are built:
|
// make sure the tldraw .css files are built:
|
||||||
await exec('yarn', ['lazy', 'prebuild'])
|
await exec('yarn', ['lazy', 'prebuild'])
|
||||||
|
|
||||||
|
@ -88,14 +81,14 @@ async function main() {
|
||||||
|
|
||||||
// deploy pre-flight steps:
|
// deploy pre-flight steps:
|
||||||
// 1. get the dotcom app ready to go (env vars and pre-build)
|
// 1. get the dotcom app ready to go (env vars and pre-build)
|
||||||
await discordStep('[2/7] building dotcom app', async () => {
|
await discord.step('building dotcom app', async () => {
|
||||||
await createSentryRelease()
|
await createSentryRelease()
|
||||||
await prepareDotcomApp()
|
await prepareDotcomApp()
|
||||||
await uploadSourceMaps()
|
await uploadSourceMaps()
|
||||||
await coalesceWithPreviousAssets(`${dotcom}/.vercel/output/static/assets`)
|
await coalesceWithPreviousAssets(`${dotcom}/.vercel/output/static/assets`)
|
||||||
})
|
})
|
||||||
|
|
||||||
await discordStep('[3/7] cloudflare deploy dry run', async () => {
|
await discord.step('cloudflare deploy dry run', async () => {
|
||||||
await deployAssetUploadWorker({ dryRun: true })
|
await deployAssetUploadWorker({ dryRun: true })
|
||||||
await deployHealthWorker({ dryRun: true })
|
await deployHealthWorker({ dryRun: true })
|
||||||
await deployTlsyncWorker({ dryRun: true })
|
await deployTlsyncWorker({ dryRun: true })
|
||||||
|
@ -103,22 +96,22 @@ async function main() {
|
||||||
|
|
||||||
// --- point of no return! do the deploy for real --- //
|
// --- point of no return! do the deploy for real --- //
|
||||||
|
|
||||||
await discordMessage(`--- **pre-flight complete, starting real deploy** ---`)
|
await discord.message(`--- **pre-flight complete, starting real dotcom deploy** ---`)
|
||||||
|
|
||||||
// 2. deploy the cloudflare workers:
|
// 2. deploy the cloudflare workers:
|
||||||
await discordStep('[4/7] deploying asset uploader to cloudflare', async () => {
|
await discord.step('deploying asset uploader to cloudflare', async () => {
|
||||||
await deployAssetUploadWorker({ dryRun: false })
|
await deployAssetUploadWorker({ dryRun: false })
|
||||||
})
|
})
|
||||||
await discordStep('[5/7] deploying multiplayer worker to cloudflare', async () => {
|
await discord.step('deploying multiplayer worker to cloudflare', async () => {
|
||||||
await deployTlsyncWorker({ dryRun: false })
|
await deployTlsyncWorker({ dryRun: false })
|
||||||
})
|
})
|
||||||
await discordStep('[6/7] deploying health worker to cloudflare', async () => {
|
await discord.step('deploying health worker to cloudflare', async () => {
|
||||||
await deployHealthWorker({ dryRun: false })
|
await deployHealthWorker({ dryRun: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. deploy the pre-build dotcom app:
|
// 3. deploy the pre-build dotcom app:
|
||||||
const { deploymentUrl, inspectUrl } = await discordStep(
|
const { deploymentUrl, inspectUrl } = await discord.step(
|
||||||
'[7/7] deploying dotcom app to vercel',
|
'deploying dotcom app to vercel',
|
||||||
async () => {
|
async () => {
|
||||||
return await deploySpa()
|
return await deploySpa()
|
||||||
}
|
}
|
||||||
|
@ -128,7 +121,7 @@ async function main() {
|
||||||
|
|
||||||
if (previewId) {
|
if (previewId) {
|
||||||
const aliasDomain = `${previewId}-preview-deploy.tldraw.com`
|
const aliasDomain = `${previewId}-preview-deploy.tldraw.com`
|
||||||
await discordStep('[8/7] aliasing preview deployment', async () => {
|
await discord.step('aliasing preview deployment', async () => {
|
||||||
await vercelCli('alias', ['set', deploymentUrl, aliasDomain])
|
await vercelCli('alias', ['set', deploymentUrl, aliasDomain])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -136,9 +129,14 @@ async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
nicelog('Creating deployment for', deploymentUrl)
|
nicelog('Creating deployment for', deploymentUrl)
|
||||||
await createGithubDeployment(deploymentAlias ?? deploymentUrl, inspectUrl)
|
await createGithubDeployment(env, {
|
||||||
|
app: 'dotcom',
|
||||||
|
deploymentUrl: deploymentAlias ?? deploymentUrl,
|
||||||
|
inspectUrl,
|
||||||
|
sha,
|
||||||
|
})
|
||||||
|
|
||||||
await discordMessage(`**Deploy complete!**`)
|
await discord.message(`**Deploy complete!**`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareDotcomApp() {
|
async function prepareDotcomApp() {
|
||||||
|
@ -167,21 +165,15 @@ async function prepareDotcomApp() {
|
||||||
let didUpdateAssetUploadWorker = false
|
let didUpdateAssetUploadWorker = false
|
||||||
async function deployAssetUploadWorker({ dryRun }: { dryRun: boolean }) {
|
async function deployAssetUploadWorker({ dryRun }: { dryRun: boolean }) {
|
||||||
if (previewId && !didUpdateAssetUploadWorker) {
|
if (previewId && !didUpdateAssetUploadWorker) {
|
||||||
appendFileSync(
|
await setWranglerPreviewConfig(assetUpload, { name: `${previewId}-tldraw-assets` })
|
||||||
join(assetUpload, 'wrangler.toml'),
|
|
||||||
`
|
|
||||||
[env.preview]
|
|
||||||
name = "${previewId}-tldraw-assets"`
|
|
||||||
)
|
|
||||||
didUpdateAssetUploadWorker = true
|
didUpdateAssetUploadWorker = true
|
||||||
}
|
}
|
||||||
await exec('yarn', ['wrangler', 'deploy', dryRun ? '--dry-run' : null, '--env', env.TLDRAW_ENV], {
|
|
||||||
pwd: assetUpload,
|
await wranglerDeploy({
|
||||||
env: {
|
location: assetUpload,
|
||||||
NODE_ENV: 'production',
|
dryRun,
|
||||||
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
env: env.TLDRAW_ENV,
|
||||||
CI: '1',
|
vars: {},
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,79 +181,39 @@ let didUpdateTlsyncWorker = false
|
||||||
async function deployTlsyncWorker({ dryRun }: { dryRun: boolean }) {
|
async function deployTlsyncWorker({ dryRun }: { dryRun: boolean }) {
|
||||||
const workerId = `${previewId ?? env.TLDRAW_ENV}-tldraw-multiplayer`
|
const workerId = `${previewId ?? env.TLDRAW_ENV}-tldraw-multiplayer`
|
||||||
if (previewId && !didUpdateTlsyncWorker) {
|
if (previewId && !didUpdateTlsyncWorker) {
|
||||||
appendFileSync(
|
await setWranglerPreviewConfig(worker, { name: workerId })
|
||||||
join(worker, 'wrangler.toml'),
|
|
||||||
`
|
|
||||||
[env.preview]
|
|
||||||
name = "${previewId}-tldraw-multiplayer"`
|
|
||||||
)
|
|
||||||
didUpdateTlsyncWorker = true
|
didUpdateTlsyncWorker = true
|
||||||
}
|
}
|
||||||
await exec(
|
await wranglerDeploy({
|
||||||
'yarn',
|
location: worker,
|
||||||
[
|
dryRun,
|
||||||
'wrangler',
|
env: env.TLDRAW_ENV,
|
||||||
'deploy',
|
vars: {
|
||||||
dryRun ? '--dry-run' : null,
|
SUPABASE_URL: env.SUPABASE_LITE_URL,
|
||||||
'--env',
|
SUPABASE_KEY: env.SUPABASE_LITE_ANON_KEY,
|
||||||
env.TLDRAW_ENV,
|
SENTRY_DSN: env.WORKER_SENTRY_DSN,
|
||||||
'--var',
|
TLDRAW_ENV: env.TLDRAW_ENV,
|
||||||
`SUPABASE_URL:${env.SUPABASE_LITE_URL}`,
|
APP_ORIGIN: env.APP_ORIGIN,
|
||||||
'--var',
|
WORKER_NAME: workerId,
|
||||||
`SUPABASE_KEY:${env.SUPABASE_LITE_ANON_KEY}`,
|
|
||||||
'--var',
|
|
||||||
`SENTRY_DSN:${env.WORKER_SENTRY_DSN}`,
|
|
||||||
'--var',
|
|
||||||
`TLDRAW_ENV:${env.TLDRAW_ENV}`,
|
|
||||||
'--var',
|
|
||||||
`APP_ORIGIN:${env.APP_ORIGIN}`,
|
|
||||||
'--var',
|
|
||||||
`WORKER_NAME:${workerId}`,
|
|
||||||
],
|
|
||||||
{
|
|
||||||
pwd: worker,
|
|
||||||
env: {
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
|
||||||
CI: '1',
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let didUpdateHealthWorker = false
|
let didUpdateHealthWorker = false
|
||||||
async function deployHealthWorker({ dryRun }: { dryRun: boolean }) {
|
async function deployHealthWorker({ dryRun }: { dryRun: boolean }) {
|
||||||
if (previewId && !didUpdateHealthWorker) {
|
if (previewId && !didUpdateHealthWorker) {
|
||||||
appendFileSync(
|
await setWranglerPreviewConfig(healthWorker, { name: `${previewId}-tldraw-health` })
|
||||||
join(healthWorker, 'wrangler.toml'),
|
|
||||||
`
|
|
||||||
[env.preview]
|
|
||||||
name = "${previewId}-tldraw-health"`
|
|
||||||
)
|
|
||||||
didUpdateHealthWorker = true
|
didUpdateHealthWorker = true
|
||||||
}
|
}
|
||||||
await exec(
|
await wranglerDeploy({
|
||||||
'yarn',
|
location: healthWorker,
|
||||||
[
|
dryRun,
|
||||||
'wrangler',
|
env: env.TLDRAW_ENV,
|
||||||
'deploy',
|
vars: {
|
||||||
dryRun ? '--dry-run' : null,
|
DISCORD_HEALTH_WEBHOOK_URL: env.DISCORD_HEALTH_WEBHOOK_URL,
|
||||||
'--env',
|
HEALTH_WORKER_UPDOWN_WEBHOOK_PATH: env.HEALTH_WORKER_UPDOWN_WEBHOOK_PATH,
|
||||||
env.TLDRAW_ENV,
|
|
||||||
'--var',
|
|
||||||
`DISCORD_HEALTH_WEBHOOK_URL:${env.DISCORD_HEALTH_WEBHOOK_URL}`,
|
|
||||||
'--var',
|
|
||||||
`HEALTH_WORKER_UPDOWN_WEBHOOK_PATH:${env.HEALTH_WORKER_UPDOWN_WEBHOOK_PATH}`,
|
|
||||||
],
|
|
||||||
{
|
|
||||||
pwd: healthWorker,
|
|
||||||
env: {
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
|
||||||
CI: '1',
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecOpts = NonNullable<Parameters<typeof exec>[2]>
|
type ExecOpts = NonNullable<Parameters<typeof exec>[2]>
|
||||||
|
@ -293,61 +245,6 @@ async function vercelCli(command: string, args: string[], opts?: ExecOpts) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeVariables(errorOutput: string): string {
|
|
||||||
const regex = /(--var\s+(\w+):[^ \n]+)/g
|
|
||||||
|
|
||||||
const sanitizedOutput = errorOutput.replace(regex, (_, match) => {
|
|
||||||
const [variable] = match.split(':')
|
|
||||||
return `${variable}:*`
|
|
||||||
})
|
|
||||||
|
|
||||||
return sanitizedOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
async function discord(method: string, url: string, body: unknown): Promise<any> {
|
|
||||||
const response = await fetch(`${env.DISCORD_DEPLOY_WEBHOOK_URL}${url}`, {
|
|
||||||
method,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Discord webhook request failed: ${response.status} ${response.statusText}`)
|
|
||||||
}
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
const AT_TEAM_MENTION = '<@&959380625100513310>'
|
|
||||||
async function discordMessage(content: string, { always = false }: { always?: boolean } = {}) {
|
|
||||||
const shouldNotify = env.TLDRAW_ENV === 'production' || always
|
|
||||||
if (!shouldNotify) {
|
|
||||||
return {
|
|
||||||
edit: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = await discord('POST', '?wait=true', { content: sanitizeVariables(content) })
|
|
||||||
|
|
||||||
return {
|
|
||||||
edit: async (newContent: string) => {
|
|
||||||
await discord('PATCH', `/messages/${message.id}`, { content: sanitizeVariables(newContent) })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function discordStep<T>(content: string, cb: () => Promise<T>): Promise<T> {
|
|
||||||
const message = await discordMessage(`${content}...`)
|
|
||||||
try {
|
|
||||||
const result = await cb()
|
|
||||||
await message.edit(`${content} ✅`)
|
|
||||||
return result
|
|
||||||
} catch (err) {
|
|
||||||
await message.edit(`${content} ❌`)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deploySpa(): Promise<{ deploymentUrl: string; inspectUrl: string }> {
|
async function deploySpa(): Promise<{ deploymentUrl: string; inspectUrl: string }> {
|
||||||
// both 'staging' and 'production' are deployed to vercel as 'production' deploys
|
// both 'staging' and 'production' are deployed to vercel as 'production' deploys
|
||||||
// in separate 'projects'
|
// in separate 'projects'
|
||||||
|
@ -371,32 +268,6 @@ async function deploySpa(): Promise<{ deploymentUrl: string; inspectUrl: string
|
||||||
return { deploymentUrl, inspectUrl }
|
return { deploymentUrl, inspectUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a github 'deployment', which creates a 'View Deployment' button in the PR timeline.
|
|
||||||
async function createGithubDeployment(deploymentUrl: string, inspectUrl: string) {
|
|
||||||
const client = github.getOctokit(env.GH_TOKEN)
|
|
||||||
|
|
||||||
const deployment = await client.rest.repos.createDeployment({
|
|
||||||
owner: 'tldraw',
|
|
||||||
repo: 'tldraw',
|
|
||||||
ref: sha,
|
|
||||||
payload: { web_url: deploymentUrl },
|
|
||||||
environment: env.TLDRAW_ENV,
|
|
||||||
transient_environment: true,
|
|
||||||
required_contexts: [],
|
|
||||||
auto_merge: false,
|
|
||||||
task: 'deploy',
|
|
||||||
})
|
|
||||||
|
|
||||||
await client.rest.repos.createDeploymentStatus({
|
|
||||||
owner: 'tldraw',
|
|
||||||
repo: 'tldraw',
|
|
||||||
deployment_id: (deployment.data as any).id,
|
|
||||||
state: 'success',
|
|
||||||
environment_url: deploymentUrl,
|
|
||||||
log_url: inspectUrl,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentryEnv = {
|
const sentryEnv = {
|
||||||
SENTRY_AUTH_TOKEN: env.SENTRY_AUTH_TOKEN,
|
SENTRY_AUTH_TOKEN: env.SENTRY_AUTH_TOKEN,
|
||||||
SENTRY_ORG: 'tldraw',
|
SENTRY_ORG: 'tldraw',
|
||||||
|
@ -526,7 +397,9 @@ async function coalesceWithPreviousAssets(assetsDir: string) {
|
||||||
main().catch(async (err) => {
|
main().catch(async (err) => {
|
||||||
// don't notify discord on preview builds
|
// don't notify discord on preview builds
|
||||||
if (env.TLDRAW_ENV !== 'preview') {
|
if (env.TLDRAW_ENV !== 'preview') {
|
||||||
await discordMessage(`${AT_TEAM_MENTION} Deploy failed: ${err.stack}`, { always: true })
|
await discord.message(`${Discord.AT_TEAM_MENTION} Deploy failed: ${err.stack}`, {
|
||||||
|
always: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
process.exit(1)
|
process.exit(1)
|
184
scripts/lib/deploy.ts
Normal file
184
scripts/lib/deploy.ts
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import * as github from '@actions/github'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { appendFile } from 'fs/promises'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { env } from 'process'
|
||||||
|
import toml from 'toml'
|
||||||
|
import { exec } from './exec'
|
||||||
|
|
||||||
|
export function getDeployInfo() {
|
||||||
|
const githubPrNumber = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/)?.[1]
|
||||||
|
|
||||||
|
let previewId = process.env.TLDRAW_PREVIEW_ID
|
||||||
|
if (!previewId && env.TLDRAW_ENV === 'preview') {
|
||||||
|
if (githubPrNumber) {
|
||||||
|
previewId = `pr-${githubPrNumber}`
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'If running preview deploys from outside of a PR action, TLDRAW_PREVIEW_ID env var must be set'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sha: string | undefined =
|
||||||
|
// if the event is 'pull_request', github.context.sha is an ephemeral merge commit
|
||||||
|
// while the actual commit we want to create the deployment for is the 'head' of the PR.
|
||||||
|
github.context.eventName === 'pull_request'
|
||||||
|
? github.context.payload.pull_request?.head.sha
|
||||||
|
: github.context.sha
|
||||||
|
|
||||||
|
if (!sha) {
|
||||||
|
throw new Error('Could not determine the SHA of the commit to deploy')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
githubPrNumber,
|
||||||
|
previewId,
|
||||||
|
sha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a github 'deployment', which creates a 'View Deployment' button in the PR timeline.
|
||||||
|
export async function createGithubDeployment(
|
||||||
|
env: { GH_TOKEN: string; TLDRAW_ENV: string },
|
||||||
|
{
|
||||||
|
app,
|
||||||
|
deploymentUrl,
|
||||||
|
inspectUrl,
|
||||||
|
sha,
|
||||||
|
}: { app: string; deploymentUrl: string; inspectUrl?: string; sha: string }
|
||||||
|
) {
|
||||||
|
const client = github.getOctokit(env.GH_TOKEN)
|
||||||
|
|
||||||
|
const deployment = await client.rest.repos.createDeployment({
|
||||||
|
owner: 'tldraw',
|
||||||
|
repo: 'tldraw',
|
||||||
|
ref: sha,
|
||||||
|
payload: { web_url: deploymentUrl },
|
||||||
|
environment: `${app}-${env.TLDRAW_ENV}`,
|
||||||
|
transient_environment: env.TLDRAW_ENV === 'preview',
|
||||||
|
production_environment: env.TLDRAW_ENV === 'production',
|
||||||
|
required_contexts: [],
|
||||||
|
auto_merge: false,
|
||||||
|
task: 'deploy',
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.rest.repos.createDeploymentStatus({
|
||||||
|
owner: 'tldraw',
|
||||||
|
repo: 'tldraw',
|
||||||
|
deployment_id: (deployment.data as any).id,
|
||||||
|
state: 'success',
|
||||||
|
environment_url: deploymentUrl,
|
||||||
|
log_url: inspectUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deploy a worker to wrangler, returning the deploy ID */
|
||||||
|
export async function wranglerDeploy({
|
||||||
|
location,
|
||||||
|
dryRun,
|
||||||
|
env,
|
||||||
|
vars,
|
||||||
|
sentry,
|
||||||
|
}: {
|
||||||
|
location: string
|
||||||
|
dryRun: boolean
|
||||||
|
env: string
|
||||||
|
vars: Record<string, string>
|
||||||
|
sentry?: {
|
||||||
|
authToken: string
|
||||||
|
project: string
|
||||||
|
release?: string
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const varsArray = []
|
||||||
|
for (const [key, value] of Object.entries(vars)) {
|
||||||
|
varsArray.push('--var', `${key}:${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = await exec(
|
||||||
|
'yarn',
|
||||||
|
[
|
||||||
|
'wrangler',
|
||||||
|
'deploy',
|
||||||
|
dryRun ? '--dry-run' : null,
|
||||||
|
'--env',
|
||||||
|
env,
|
||||||
|
'--outdir',
|
||||||
|
'.wrangler/dist',
|
||||||
|
...varsArray,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
pwd: location,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
// wrangler needs CI=1 set to prevent it from trying to do interactive prompts
|
||||||
|
CI: '1',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (dryRun) return
|
||||||
|
|
||||||
|
const versionMatch = out.match(/Current Version ID: (.+)/)
|
||||||
|
if (!versionMatch) {
|
||||||
|
throw new Error('Could not find the deploy ID in wrangler output')
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerName = toml.parse(readFileSync(join(location, 'wrangler.toml')).toString())?.env?.[
|
||||||
|
env
|
||||||
|
]?.name
|
||||||
|
|
||||||
|
if (!workerName) {
|
||||||
|
throw new Error('Could not find the worker name in wrangler output')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentry) {
|
||||||
|
const release = sentry.release ?? `${workerName}.${versionMatch[1]}`
|
||||||
|
|
||||||
|
const sentryEnv = {
|
||||||
|
SENTRY_AUTH_TOKEN: sentry.authToken,
|
||||||
|
SENTRY_ORG: 'tldraw',
|
||||||
|
SENTRY_PROJECT: sentry.project,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a sentry release:
|
||||||
|
exec('yarn', ['run', '-T', 'sentry-cli', 'releases', 'new', release], {
|
||||||
|
pwd: location,
|
||||||
|
env: sentryEnv,
|
||||||
|
})
|
||||||
|
|
||||||
|
// upload sourcemaps to the release:
|
||||||
|
exec(
|
||||||
|
'yarn',
|
||||||
|
[
|
||||||
|
'run',
|
||||||
|
'-T',
|
||||||
|
'sentry-cli',
|
||||||
|
'releases',
|
||||||
|
'files',
|
||||||
|
release,
|
||||||
|
'upload-sourcemaps',
|
||||||
|
'.wrangler/dist',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
pwd: location,
|
||||||
|
env: sentryEnv,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setWranglerPreviewConfig(
|
||||||
|
location: string,
|
||||||
|
{ name, customDomain }: { name: string; customDomain?: string }
|
||||||
|
) {
|
||||||
|
await appendFile(
|
||||||
|
join(location, 'wrangler.toml'),
|
||||||
|
`
|
||||||
|
[env.preview]
|
||||||
|
name = "${name}"
|
||||||
|
${customDomain ? `routes = [ { pattern = "${customDomain}", custom_domain = true} ]` : ''}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
}
|
70
scripts/lib/discord.ts
Normal file
70
scripts/lib/discord.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
function sanitizeVariables(errorOutput: string): string {
|
||||||
|
const regex = /(--var\s+(\w+):[^ \n]+)/g
|
||||||
|
|
||||||
|
const sanitizedOutput = errorOutput.replace(regex, (_, match) => {
|
||||||
|
const [variable] = match.split(':')
|
||||||
|
return `${variable}:*`
|
||||||
|
})
|
||||||
|
|
||||||
|
return sanitizedOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Discord {
|
||||||
|
static AT_TEAM_MENTION = '<@&959380625100513310>'
|
||||||
|
|
||||||
|
constructor(opts: { webhookUrl: string; shouldNotify: boolean; totalSteps?: number }) {
|
||||||
|
this.webhookUrl = opts.webhookUrl
|
||||||
|
this.shouldNotify = opts.shouldNotify
|
||||||
|
this.totalSteps = opts.totalSteps ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookUrl: string
|
||||||
|
shouldNotify: boolean
|
||||||
|
totalSteps: number
|
||||||
|
currentStep = 0
|
||||||
|
|
||||||
|
private async send(method: string, url: string, body: unknown): Promise<any> {
|
||||||
|
const response = await fetch(`${this.webhookUrl}${url}`, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Discord webhook request failed: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async message(content: string, { always = false }: { always?: boolean } = {}) {
|
||||||
|
if (!always && !this.shouldNotify) {
|
||||||
|
return {
|
||||||
|
edit: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await this.send('POST', '?wait=true', { content: sanitizeVariables(content) })
|
||||||
|
|
||||||
|
return {
|
||||||
|
edit: async (newContent: string) => {
|
||||||
|
await this.send('PATCH', `/messages/${message.id}`, {
|
||||||
|
content: sanitizeVariables(newContent),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async step<T>(content: string, cb: () => Promise<T>): Promise<T> {
|
||||||
|
this.currentStep++
|
||||||
|
const message = await this.message(`[${this.currentStep}/${this.totalSteps}] ${content}...`)
|
||||||
|
try {
|
||||||
|
const result = await cb()
|
||||||
|
await message.edit(`${content} ✅`)
|
||||||
|
return result
|
||||||
|
} catch (err) {
|
||||||
|
await message.edit(`${content} ❌`)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@
|
||||||
"@typescript-eslint/utils": "^5.59.0",
|
"@typescript-eslint/utils": "^5.59.0",
|
||||||
"ast-types": "^0.14.2",
|
"ast-types": "^0.14.2",
|
||||||
"cross-fetch": "^3.1.5",
|
"cross-fetch": "^3.1.5",
|
||||||
"esbuild": "^0.18.4",
|
"esbuild": "^0.21.5",
|
||||||
"eslint": "^8.37.0",
|
"eslint": "^8.37.0",
|
||||||
"glob": "^8.0.3",
|
"glob": "^8.0.3",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
@ -59,6 +59,7 @@
|
||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"tar": "^7.0.1",
|
"tar": "^7.0.1",
|
||||||
"tmp": "^0.2.3"
|
"tmp": "^0.2.3",
|
||||||
|
"toml": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
|
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
||||||
|
import kleur from 'kleur'
|
||||||
|
import stripAnsi from 'strip-ansi'
|
||||||
|
|
||||||
// at the time of writing, workerd will regularly crash with a segfault
|
// at the time of writing, workerd will regularly crash with a segfault
|
||||||
// but the error is not caught by the process, so it will just hang
|
// but the error is not caught by the process, so it will just hang
|
||||||
// this script wraps the process, tailing the logs and restarting the process
|
// this script wraps the process, tailing the logs and restarting the process
|
||||||
// if we encounter the string 'Segmentation fault'
|
// if we encounter the string 'Segmentation fault'
|
||||||
|
|
||||||
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
|
||||||
import stripAnsi from 'strip-ansi'
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
const log = console.log
|
|
||||||
|
|
||||||
class MiniflareMonitor {
|
class MiniflareMonitor {
|
||||||
private process: ChildProcessWithoutNullStreams | null = null
|
private process: ChildProcessWithoutNullStreams | null = null
|
||||||
|
|
||||||
|
@ -19,7 +16,7 @@ class MiniflareMonitor {
|
||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
this.stop() // Ensure any existing process is stopped
|
this.stop() // Ensure any existing process is stopped
|
||||||
log(`Starting wrangler...`)
|
console.log(`Starting wrangler...`)
|
||||||
this.process = spawn(this.command, this.args, {
|
this.process = spawn(this.command, this.args, {
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'development',
|
NODE_ENV: 'development',
|
||||||
|
@ -42,12 +39,12 @@ class MiniflareMonitor {
|
||||||
console.error('Segmentation fault detected. Restarting Miniflare...')
|
console.error('Segmentation fault detected. Restarting Miniflare...')
|
||||||
this.restart()
|
this.restart()
|
||||||
} else if (!err) {
|
} else if (!err) {
|
||||||
log(output.replace('[mf:inf]', '')) // or handle the output differently
|
console.log(output.replace('[mf:inf]', '')) // or handle the output differently
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private restart(): void {
|
private restart(): void {
|
||||||
log('Restarting wrangler...')
|
console.log('Restarting wrangler...')
|
||||||
this.stop()
|
this.stop()
|
||||||
setTimeout(() => this.start(), 3000) // Restart after a short delay
|
setTimeout(() => this.start(), 3000) // Restart after a short delay
|
||||||
}
|
}
|
||||||
|
@ -60,7 +57,60 @@ class MiniflareMonitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const monitor = new MiniflareMonitor('wrangler', [
|
class SizeReporter {
|
||||||
|
lastLineTime = Date.now()
|
||||||
|
nextTick?: NodeJS.Timeout
|
||||||
|
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
start() {
|
||||||
|
console.log('Spawning size reporter...')
|
||||||
|
const proc = spawn('yarn', [
|
||||||
|
'run',
|
||||||
|
'-T',
|
||||||
|
'esbuild',
|
||||||
|
'src/worker.ts',
|
||||||
|
'--bundle',
|
||||||
|
'--minify',
|
||||||
|
'--watch',
|
||||||
|
'--external:cloudflare:*',
|
||||||
|
'--target=esnext',
|
||||||
|
'--format=esm',
|
||||||
|
])
|
||||||
|
// listen for lines on stdin
|
||||||
|
proc.stdout.on('data', (data) => {
|
||||||
|
this.size += data.length
|
||||||
|
this.lastLineTime = Date.now()
|
||||||
|
clearTimeout(this.nextTick)
|
||||||
|
this.nextTick = setTimeout(() => {
|
||||||
|
console.log(
|
||||||
|
kleur.bold(kleur.yellow('worker')),
|
||||||
|
'is roughly',
|
||||||
|
kleur.bold(kleur.cyan(Math.floor(this.size / 1024) + 'kb')),
|
||||||
|
'(minified)\n'
|
||||||
|
)
|
||||||
|
this.size = 0
|
||||||
|
}, 10)
|
||||||
|
})
|
||||||
|
proc.stderr.on('data', (data) => {
|
||||||
|
console.log(data.toString())
|
||||||
|
})
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('Int')
|
||||||
|
proc.kill()
|
||||||
|
})
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('Term')
|
||||||
|
proc.kill()
|
||||||
|
})
|
||||||
|
process.on('exit', () => {
|
||||||
|
console.log('Exiting')
|
||||||
|
proc.kill()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new MiniflareMonitor('wrangler', [
|
||||||
'dev',
|
'dev',
|
||||||
'--env',
|
'--env',
|
||||||
'dev',
|
'dev',
|
||||||
|
@ -69,5 +119,6 @@ const monitor = new MiniflareMonitor('wrangler', [
|
||||||
'info',
|
'info',
|
||||||
'--var',
|
'--var',
|
||||||
'IS_LOCAL:true',
|
'IS_LOCAL:true',
|
||||||
])
|
]).start()
|
||||||
monitor.start()
|
|
||||||
|
new SizeReporter().start()
|
Loading…
Reference in a new issue