| author | |
| committer | |
| log | aa8b9f2286fd8c4fcc36a240e34e8e89a3d793fa |
| tree | 6473fa52331efef3298e0ac83b301e5093e7d242 |
| parent | 073eb389d3b831ea3622fbb1a98a9fea6454a6ba |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>14 files changed, 348 insertions(+), 44 deletions(-)
.github/workflows/release.yml+86-1| ... | @@ -567,6 +567,90 @@ jobs: | ... | @@ -567,6 +567,90 @@ jobs: |
| 567 | fail_on_unmatched_files: true | 567 | fail_on_unmatched_files: true |
| 568 | token: ${{ steps.app_token.outputs.token }} | 568 | token: ${{ steps.app_token.outputs.token }} |
| 569 | 569 | ||
| 570 | deploy_web: | ||
| 571 | name: Deploy hosted web app | ||
| 572 | needs: [preflight, release] | ||
| 573 | if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }} | ||
| 574 | runs-on: blacksmith-8vcpu-ubuntu-2404 | ||
| 575 | timeout-minutes: 10 | ||
| 576 | env: | ||
| 577 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | ||
| 578 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | ||
| 579 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | ||
| 580 | T3CODE_WEB_ROUTER_URL: ${{ vars.T3CODE_WEB_ROUTER_URL }} | ||
| 581 | T3CODE_WEB_LATEST_DOMAIN: ${{ vars.T3CODE_WEB_LATEST_DOMAIN }} | ||
| 582 | T3CODE_WEB_NIGHTLY_DOMAIN: ${{ vars.T3CODE_WEB_NIGHTLY_DOMAIN }} | ||
| 583 | VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} | ||
| 584 | steps: | ||
| 585 | - name: Checkout | ||
| 586 | uses: actions/checkout@v6 | ||
| 587 | with: | ||
| 588 | ref: ${{ needs.preflight.outputs.ref }} | ||
| 589 | |||
| 590 | - name: Setup Bun | ||
| 591 | uses: oven-sh/setup-bun@v2 | ||
| 592 | with: | ||
| 593 | bun-version-file: package.json | ||
| 594 | |||
| 595 | - name: Setup Node | ||
| 596 | uses: actions/setup-node@v6 | ||
| 597 | with: | ||
| 598 | node-version-file: package.json | ||
| 599 | |||
| 600 | - name: Install release tooling dependencies | ||
| 601 | run: bun install --frozen-lockfile --filter=@t3tools/scripts --filter=@t3tools/web | ||
| 602 | |||
| 603 | - name: Align package versions to release version | ||
| 604 | run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" | ||
| 605 | |||
| 606 | - name: Refresh release lockfile | ||
| 607 | run: bun install --lockfile-only --ignore-scripts | ||
| 608 | |||
| 609 | - name: Deploy and alias channel | ||
| 610 | shell: bash | ||
| 611 | run: | | ||
| 612 | set -euo pipefail | ||
| 613 | |||
| 614 | if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_ORG_ID:-}" || -z "${VERCEL_PROJECT_ID:-}" ]]; then | ||
| 615 | echo "Missing one or more required Vercel secrets: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID." >&2 | ||
| 616 | exit 1 | ||
| 617 | fi | ||
| 618 | |||
| 619 | router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}" | ||
| 620 | latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}" | ||
| 621 | nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}" | ||
| 622 | |||
| 623 | if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then | ||
| 624 | channel_domain="$latest_domain" | ||
| 625 | channel_name="latest" | ||
| 626 | else | ||
| 627 | channel_domain="$nightly_domain" | ||
| 628 | channel_name="nightly" | ||
| 629 | fi | ||
| 630 | |||
| 631 | vercel_scope_args=() | ||
| 632 | if [[ -n "${VERCEL_TEAM_SLUG:-}" ]]; then | ||
| 633 | vercel_scope_args=(--scope "$VERCEL_TEAM_SLUG") | ||
| 634 | fi | ||
| 635 | |||
| 636 | echo "Deploying hosted web app for $channel_name channel." | ||
| 637 | deployment_url="$( | ||
| 638 | bunx vercel@53.1.1 deploy apps/web \ | ||
| 639 | --prod \ | ||
| 640 | --skip-domain \ | ||
| 641 | --yes \ | ||
| 642 | --token "$VERCEL_TOKEN" \ | ||
| 643 | "${vercel_scope_args[@]}" \ | ||
| 644 | --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \ | ||
| 645 | --build-env "VITE_HOSTED_APP_URL=$router_url" \ | ||
| 646 | --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name" | ||
| 647 | )" | ||
| 648 | |||
| 649 | echo "Aliasing $deployment_url to $channel_domain." | ||
| 650 | bunx vercel@53.1.1 alias set "$deployment_url" "$channel_domain" \ | ||
| 651 | --token "$VERCEL_TOKEN" \ | ||
| 652 | "${vercel_scope_args[@]}" | ||
| 653 | |||
| 570 | finalize: | 654 | finalize: |
| 571 | name: Finalize release | 655 | name: Finalize release |
| 572 | if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }} | 656 | if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }} |
| ... | @@ -651,8 +735,9 @@ jobs: | ... | @@ -651,8 +735,9 @@ jobs: |
| 651 | always() && !cancelled() && | 735 | always() && !cancelled() && |
| 652 | needs.preflight.result == 'success' && | 736 | needs.preflight.result == 'success' && |
| 653 | needs.release.result == 'success' && | 737 | needs.release.result == 'success' && |
| 738 | needs.deploy_web.result == 'success' && | ||
| 654 | (needs.finalize.result == 'success' || needs.finalize.result == 'skipped') | 739 | (needs.finalize.result == 'success' || needs.finalize.result == 'skipped') |
| 655 | needs: [preflight, release, finalize] | 740 | needs: [preflight, release, deploy_web, finalize] |
| 656 | runs-on: blacksmith-8vcpu-ubuntu-2404 | 741 | runs-on: blacksmith-8vcpu-ubuntu-2404 |
| 657 | timeout-minutes: 10 | 742 | timeout-minutes: 10 |
| 658 | steps: | 743 | steps: |
apps/web/package.json+1| ... | @@ -50,6 +50,7 @@ | ... | @@ -50,6 +50,7 @@ |
| 50 | "@types/babel__core": "^7.20.5", | 50 | "@types/babel__core": "^7.20.5", |
| 51 | "@types/react": "^19.0.0", | 51 | "@types/react": "^19.0.0", |
| 52 | "@types/react-dom": "^19.0.0", | 52 | "@types/react-dom": "^19.0.0", |
| 53 | "@vercel/config": "^0.3.0", | ||
| 53 | "@vitejs/plugin-react": "^6.0.0", | 54 | "@vitejs/plugin-react": "^6.0.0", |
| 54 | "@vitest/browser-playwright": "^4.0.18", | 55 | "@vitest/browser-playwright": "^4.0.18", |
| 55 | "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", | 56 | "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", |
apps/web/src/branding.test.ts+18| ... | @@ -34,4 +34,22 @@ describe("branding", () => { | ... | @@ -34,4 +34,22 @@ describe("branding", () => { |
| 34 | expect(branding.APP_STAGE_LABEL).toBe("Nightly"); | 34 | expect(branding.APP_STAGE_LABEL).toBe("Nightly"); |
| 35 | expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)"); | 35 | expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)"); |
| 36 | }); | 36 | }); |
| 37 | |||
| 38 | it("normalizes hosted app channel metadata", async () => { | ||
| 39 | vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "nightly"); | ||
| 40 | |||
| 41 | const branding = await import("./branding"); | ||
| 42 | |||
| 43 | expect(branding.HOSTED_APP_CHANNEL).toBe("nightly"); | ||
| 44 | expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly"); | ||
| 45 | }); | ||
| 46 | |||
| 47 | it("ignores unknown hosted app channels", async () => { | ||
| 48 | vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview"); | ||
| 49 | |||
| 50 | const branding = await import("./branding"); | ||
| 51 | |||
| 52 | expect(branding.HOSTED_APP_CHANNEL).toBeNull(); | ||
| 53 | expect(branding.HOSTED_APP_CHANNEL_LABEL).toBeNull(); | ||
| 54 | }); | ||
| 37 | }); | 55 | }); |
apps/web/src/branding.ts+5| ... | @@ -9,6 +9,7 @@ function readInjectedDesktopAppBranding(): DesktopAppBranding | null { | ... | @@ -9,6 +9,7 @@ function readInjectedDesktopAppBranding(): DesktopAppBranding | null { |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | const injectedDesktopAppBranding = readInjectedDesktopAppBranding(); | 11 | const injectedDesktopAppBranding = readInjectedDesktopAppBranding(); |
| 12 | const hostedAppChannel = import.meta.env.VITE_HOSTED_APP_CHANNEL?.trim().toLowerCase(); | ||
| 12 | 13 | ||
| 13 | export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code"; | 14 | export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code"; |
| 14 | export const APP_STAGE_LABEL = | 15 | export const APP_STAGE_LABEL = |
| ... | @@ -16,3 +17,7 @@ export const APP_STAGE_LABEL = | ... | @@ -16,3 +17,7 @@ export const APP_STAGE_LABEL = |
| 16 | export const APP_DISPLAY_NAME = | 17 | export const APP_DISPLAY_NAME = |
| 17 | injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`; | 18 | injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`; |
| 18 | export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; | 19 | export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; |
| 20 | export const HOSTED_APP_CHANNEL = | ||
| 21 | hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null; | ||
| 22 | export const HOSTED_APP_CHANNEL_LABEL = | ||
| 23 | HOSTED_APP_CHANNEL === "nightly" ? "Nightly" : HOSTED_APP_CHANNEL === "latest" ? "Latest" : null; |
apps/web/src/components/settings/SettingsPanels.tsx+62-30| ... | @@ -15,7 +15,7 @@ import { scopeThreadRef } from "@t3tools/client-runtime"; | ... | @@ -15,7 +15,7 @@ import { scopeThreadRef } from "@t3tools/client-runtime"; |
| 15 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; | 15 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; |
| 16 | import { createModelSelection } from "@t3tools/shared/model"; | 16 | import { createModelSelection } from "@t3tools/shared/model"; |
| 17 | import * as Equal from "effect/Equal"; | 17 | import * as Equal from "effect/Equal"; |
| 18 | import { APP_VERSION } from "../../branding"; | 18 | import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; |
| 19 | import { | 19 | import { |
| 20 | canCheckForUpdate, | 20 | canCheckForUpdate, |
| 21 | getDesktopUpdateButtonTooltip, | 21 | getDesktopUpdateButtonTooltip, |
| ... | @@ -26,6 +26,7 @@ import { | ... | @@ -26,6 +26,7 @@ import { |
| 26 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; | 26 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; |
| 27 | import { TraitsPicker } from "../chat/TraitsPicker"; | 27 | import { TraitsPicker } from "../chat/TraitsPicker"; |
| 28 | import { isElectron } from "../../env"; | 28 | import { isElectron } from "../../env"; |
| 29 | import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; | ||
| 29 | import { useTheme } from "../../hooks/useTheme"; | 30 | import { useTheme } from "../../hooks/useTheme"; |
| 30 | import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; | 31 | import { useSettings, useUpdateSettings } from "../../hooks/useSettings"; |
| 31 | import { useThreadActions } from "../../hooks/useThreadActions"; | 32 | import { useThreadActions } from "../../hooks/useThreadActions"; |
| ... | @@ -162,6 +163,7 @@ function AboutVersionSection() { | ... | @@ -162,6 +163,7 @@ function AboutVersionSection() { |
| 162 | const updateState = updateStateQuery.data ?? null; | 163 | const updateState = updateStateQuery.data ?? null; |
| 163 | const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge); | 164 | const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge); |
| 164 | const selectedUpdateChannel = updateState?.channel ?? "latest"; | 165 | const selectedUpdateChannel = updateState?.channel ?? "latest"; |
| 166 | const selectedHostedAppChannel = hasDesktopBridge ? null : HOSTED_APP_CHANNEL; | ||
| 165 | 167 | ||
| 166 | const handleUpdateChannelChange = useCallback( | 168 | const handleUpdateChannelChange = useCallback( |
| 167 | (channel: DesktopUpdateChannel) => { | 169 | (channel: DesktopUpdateChannel) => { |
| ... | @@ -314,36 +316,66 @@ function AboutVersionSection() { | ... | @@ -314,36 +316,66 @@ function AboutVersionSection() { |
| 314 | </Tooltip> | 316 | </Tooltip> |
| 315 | } | 317 | } |
| 316 | /> | 318 | /> |
| 317 | <SettingsRow | 319 | {hasDesktopBridge ? ( |
| 318 | title="Update track" | 320 | <SettingsRow |
| 319 | description="Stable follows full releases. Nightly follows the nightly desktop channel and can switch back to stable immediately." | 321 | title="Update track" |
| 320 | control={ | 322 | description="Stable follows full releases. Nightly follows the nightly desktop channel and can switch back to stable immediately." |
| 321 | <Select | 323 | control={ |
| 322 | value={selectedUpdateChannel} | 324 | <Select |
| 323 | onValueChange={(value) => { | 325 | value={selectedUpdateChannel} |
| 324 | handleUpdateChannelChange(value as DesktopUpdateChannel); | 326 | onValueChange={(value) => { |
| 325 | }} | 327 | handleUpdateChannelChange(value as DesktopUpdateChannel); |
| 326 | > | 328 | }} |
| 327 | <SelectTrigger | ||
| 328 | className="w-full sm:w-40" | ||
| 329 | aria-label="Update track" | ||
| 330 | disabled={!hasDesktopBridge || isChangingUpdateChannel} | ||
| 331 | > | 329 | > |
| 332 | <SelectValue> | 330 | <SelectTrigger |
| 333 | {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"} | 331 | className="w-full sm:w-40" |
| 334 | </SelectValue> | 332 | aria-label="Update track" |
| 335 | </SelectTrigger> | 333 | disabled={isChangingUpdateChannel} |
| 336 | <SelectPopup align="end" alignItemWithTrigger={false}> | 334 | > |
| 337 | <SelectItem hideIndicator value="latest"> | 335 | <SelectValue> |
| 338 | Stable | 336 | {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"} |
| 339 | </SelectItem> | 337 | </SelectValue> |
| 340 | <SelectItem hideIndicator value="nightly"> | 338 | </SelectTrigger> |
| 341 | Nightly | 339 | <SelectPopup align="end" alignItemWithTrigger={false}> |
| 342 | </SelectItem> | 340 | <SelectItem hideIndicator value="latest"> |
| 343 | </SelectPopup> | 341 | Stable |
| 344 | </Select> | 342 | </SelectItem> |
| 345 | } | 343 | <SelectItem hideIndicator value="nightly"> |
| 346 | /> | 344 | Nightly |
| 345 | </SelectItem> | ||
| 346 | </SelectPopup> | ||
| 347 | </Select> | ||
| 348 | } | ||
| 349 | /> | ||
| 350 | ) : selectedHostedAppChannel ? ( | ||
| 351 | <SettingsRow | ||
| 352 | title="Update track" | ||
| 353 | description="Switches the hosted app release channel." | ||
| 354 | control={ | ||
| 355 | <Select | ||
| 356 | value={selectedHostedAppChannel} | ||
| 357 | onValueChange={(value) => { | ||
| 358 | if (value === selectedHostedAppChannel) return; | ||
| 359 | window.location.assign( | ||
| 360 | buildHostedChannelSelectionUrl({ channel: value as HostedAppChannel }), | ||
| 361 | ); | ||
| 362 | }} | ||
| 363 | > | ||
| 364 | <SelectTrigger className="w-full sm:w-40" aria-label="Update track"> | ||
| 365 | <SelectValue>{HOSTED_APP_CHANNEL_LABEL}</SelectValue> | ||
| 366 | </SelectTrigger> | ||
| 367 | <SelectPopup align="end" alignItemWithTrigger={false}> | ||
| 368 | <SelectItem hideIndicator value="latest"> | ||
| 369 | Latest | ||
| 370 | </SelectItem> | ||
| 371 | <SelectItem hideIndicator value="nightly"> | ||
| 372 | Nightly | ||
| 373 | </SelectItem> | ||
| 374 | </SelectPopup> | ||
| 375 | </Select> | ||
| 376 | } | ||
| 377 | /> | ||
| 378 | ) : null} | ||
| 347 | </> | 379 | </> |
| 348 | ); | 380 | ); |
| 349 | } | 381 | } |
apps/web/src/hostedPairing.test.ts+16| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import { afterEach, describe, expect, it, vi } from "vitest"; | 1 | import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | buildHostedChannelSelectionUrl, | ||
| 4 | buildHostedPairingUrl, | 5 | buildHostedPairingUrl, |
| 5 | hasHostedPairingRequest, | 6 | hasHostedPairingRequest, |
| 6 | isHostedStaticApp, | 7 | isHostedStaticApp, |
| ... | @@ -42,6 +43,21 @@ describe("hostedPairing", () => { | ... | @@ -42,6 +43,21 @@ describe("hostedPairing", () => { |
| 42 | expect(url.hash).toBe("#token=pairing-token"); | 43 | expect(url.hash).toBe("#token=pairing-token"); |
| 43 | }); | 44 | }); |
| 44 | 45 | ||
| 46 | it("builds hosted channel selection URLs through the configured router origin", () => { | ||
| 47 | vi.stubEnv("VITE_HOSTED_APP_URL", "https://app.t3.codes"); | ||
| 48 | |||
| 49 | const url = new URL( | ||
| 50 | buildHostedChannelSelectionUrl({ | ||
| 51 | channel: "nightly", | ||
| 52 | }), | ||
| 53 | ); | ||
| 54 | |||
| 55 | expect(url.origin).toBe("https://app.t3.codes"); | ||
| 56 | expect(url.pathname).toBe("/__t3code/channel"); | ||
| 57 | expect(url.searchParams.get("channel")).toBe("nightly"); | ||
| 58 | expect(url.searchParams.has("next")).toBe(false); | ||
| 59 | }); | ||
| 60 | |||
| 45 | it("ignores incomplete hosted pairing requests", () => { | 61 | it("ignores incomplete hosted pairing requests", () => { |
| 46 | expect( | 62 | expect( |
| 47 | hasHostedPairingRequest(new URL("https://app.t3.codes/pair?host=backend.example.com")), | 63 | hasHostedPairingRequest(new URL("https://app.t3.codes/pair?host=backend.example.com")), |
apps/web/src/hostedPairing.ts+10| ... | @@ -8,6 +8,8 @@ export interface HostedPairingRequest { | ... | @@ -8,6 +8,8 @@ export interface HostedPairingRequest { |
| 8 | readonly label: string; | 8 | readonly label: string; |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | export type HostedAppChannel = "latest" | "nightly"; | ||
| 12 | |||
| 11 | function configuredHostedAppUrl(): string { | 13 | function configuredHostedAppUrl(): string { |
| 12 | return import.meta.env.VITE_HOSTED_APP_URL?.trim() || DEFAULT_HOSTED_APP_URL; | 14 | return import.meta.env.VITE_HOSTED_APP_URL?.trim() || DEFAULT_HOSTED_APP_URL; |
| 13 | } | 15 | } |
| ... | @@ -68,3 +70,11 @@ export function buildHostedPairingUrl(input: { | ... | @@ -68,3 +70,11 @@ export function buildHostedPairingUrl(input: { |
| 68 | 70 | ||
| 69 | return setPairingTokenOnUrl(url, input.token).toString(); | 71 | return setPairingTokenOnUrl(url, input.token).toString(); |
| 70 | } | 72 | } |
| 73 | |||
| 74 | export function buildHostedChannelSelectionUrl(input: { | ||
| 75 | readonly channel: HostedAppChannel; | ||
| 76 | }): string { | ||
| 77 | const url = new URL("/__t3code/channel", configuredHostedAppUrl()); | ||
| 78 | url.searchParams.set("channel", input.channel); | ||
| 79 | return url.toString(); | ||
| 80 | } |
apps/web/src/vite-env.d.ts+1| ... | @@ -6,6 +6,7 @@ interface ImportMetaEnv { | ... | @@ -6,6 +6,7 @@ interface ImportMetaEnv { |
| 6 | readonly VITE_HTTP_URL: string; | 6 | readonly VITE_HTTP_URL: string; |
| 7 | readonly VITE_WS_URL: string; | 7 | readonly VITE_WS_URL: string; |
| 8 | readonly VITE_HOSTED_APP_URL: string; | 8 | readonly VITE_HOSTED_APP_URL: string; |
| 9 | readonly VITE_HOSTED_APP_CHANNEL: string; | ||
| 9 | readonly APP_VERSION: string; | 10 | readonly APP_VERSION: string; |
| 10 | } | 11 | } |
| 11 | 12 |
apps/web/tsconfig.json+1-1| ... | @@ -35,5 +35,5 @@ | ... | @@ -35,5 +35,5 @@ |
| 35 | } | 35 | } |
| 36 | ] | 36 | ] |
| 37 | }, | 37 | }, |
| 38 | "include": ["src", "vite.config.ts", "test"] | 38 | "include": ["src", "vite.config.ts", "vercel.ts", "test"] |
| 39 | } | 39 | } |
apps/web/vercel.json deleted-10| ... | @@ -1,10 +0,0 @@ | ||
| 1 | { | ||
| 2 | "buildCommand": "turbo build --filter @t3tools/web && bun ../../scripts/apply-web-brand-assets.ts production", | ||
| 3 | "installCommand": "bun add -g turbo && bun install --filter '@t3tools/contracts' --filter '@t3tools/client-runtime' --filter '@t3tools/scripts' --filter '@t3tools/web'", | ||
| 4 | "rewrites": [ | ||
| 5 | { | ||
| 6 | "source": "/(.*)", | ||
| 7 | "destination": "/index.html" | ||
| 8 | } | ||
| 9 | ] | ||
| 10 | } | ||
apps/web/vercel.ts created+57| ... | @@ -0,0 +1,57 @@ | ||
| 1 | import { matchers, routes, type VercelConfig } from "@vercel/config/v1"; | ||
| 2 | |||
| 3 | const ROUTER_HOST = "app.t3.codes"; | ||
| 4 | const HOSTED_WEB_CHANNEL_COOKIE = "t3code_web_channel"; | ||
| 5 | const LATEST_ORIGIN = "https://latest.app.t3.codes"; | ||
| 6 | const NIGHTLY_ORIGIN = "https://nightly.app.t3.codes"; | ||
| 7 | |||
| 8 | function channelCookie(channel: "latest" | "nightly"): string { | ||
| 9 | return [ | ||
| 10 | `${HOSTED_WEB_CHANNEL_COOKIE}=${channel}`, | ||
| 11 | "Path=/", | ||
| 12 | "Max-Age=31536000", | ||
| 13 | "HttpOnly", | ||
| 14 | "Secure", | ||
| 15 | "SameSite=Lax", | ||
| 16 | ].join("; "); | ||
| 17 | } | ||
| 18 | |||
| 19 | export const config: VercelConfig = { | ||
| 20 | buildCommand: | ||
| 21 | "turbo build --filter @t3tools/web && bun ../../scripts/apply-web-brand-assets.ts production", | ||
| 22 | git: { | ||
| 23 | deploymentEnabled: false, | ||
| 24 | }, | ||
| 25 | installCommand: | ||
| 26 | "bun add -g turbo && bun install --filter '@t3tools/contracts' --filter '@t3tools/client-runtime' --filter '@t3tools/scripts' --filter '@t3tools/web'", | ||
| 27 | routes: [ | ||
| 28 | { | ||
| 29 | src: "/__t3code/channel", | ||
| 30 | has: [matchers.query("channel", "nightly")], | ||
| 31 | headers: { | ||
| 32 | Location: "/", | ||
| 33 | "Set-Cookie": channelCookie("nightly"), | ||
| 34 | }, | ||
| 35 | status: 302, | ||
| 36 | }, | ||
| 37 | { | ||
| 38 | src: "/__t3code/channel", | ||
| 39 | headers: { | ||
| 40 | Location: "/", | ||
| 41 | "Set-Cookie": channelCookie("latest"), | ||
| 42 | }, | ||
| 43 | status: 302, | ||
| 44 | }, | ||
| 45 | { | ||
| 46 | src: "/(.*)", | ||
| 47 | has: [matchers.host(ROUTER_HOST), matchers.cookie(HOSTED_WEB_CHANNEL_COOKIE, "nightly")], | ||
| 48 | dest: `${NIGHTLY_ORIGIN}/$1`, | ||
| 49 | }, | ||
| 50 | { | ||
| 51 | src: "/(.*)", | ||
| 52 | has: [matchers.host(ROUTER_HOST)], | ||
| 53 | dest: `${LATEST_ORIGIN}/$1`, | ||
| 54 | }, | ||
| 55 | ], | ||
| 56 | rewrites: [routes.rewrite("/(.*)", "/index.html")], | ||
| 57 | }; | ||
apps/web/vite.config.ts+9-2| ... | @@ -8,14 +8,20 @@ import pkg from "./package.json" with { type: "json" }; | ... | @@ -8,14 +8,20 @@ import pkg from "./package.json" with { type: "json" }; |
| 8 | const port = Number(process.env.PORT ?? 5733); | 8 | const port = Number(process.env.PORT ?? 5733); |
| 9 | const host = process.env.HOST?.trim() || "localhost"; | 9 | const host = process.env.HOST?.trim() || "localhost"; |
| 10 | const configuredWsUrl = process.env.VITE_WS_URL?.trim(); | 10 | const configuredWsUrl = process.env.VITE_WS_URL?.trim(); |
| 11 | const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || ""; | ||
| 12 | const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version; | ||
| 11 | const configuredHostedAppUrl = (() => { | 13 | const configuredHostedAppUrl = (() => { |
| 14 | const explicitHostedAppUrl = process.env.VITE_HOSTED_APP_URL?.trim(); | ||
| 15 | if (explicitHostedAppUrl) { | ||
| 16 | return explicitHostedAppUrl; | ||
| 17 | } | ||
| 12 | if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) { | 18 | if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) { |
| 13 | return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`; | 19 | return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`; |
| 14 | } | 20 | } |
| 15 | if (process.env.VERCEL_URL) { | 21 | if (process.env.VERCEL_URL) { |
| 16 | return `https://${process.env.VERCEL_URL}`; | 22 | return `https://${process.env.VERCEL_URL}`; |
| 17 | } | 23 | } |
| 18 | return process.env.VITE_HOSTED_APP_URL?.trim(); | 24 | return undefined; |
| 19 | })(); | 25 | })(); |
| 20 | const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase(); | 26 | const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase(); |
| 21 | 27 | ||
| ... | @@ -76,7 +82,8 @@ export default defineConfig({ | ... | @@ -76,7 +82,8 @@ export default defineConfig({ |
| 76 | // In dev mode, tell the web app where the WebSocket server lives | 82 | // In dev mode, tell the web app where the WebSocket server lives |
| 77 | "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), | 83 | "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), |
| 78 | "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""), | 84 | "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""), |
| 79 | "import.meta.env.APP_VERSION": JSON.stringify(pkg.version), | 85 | "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel), |
| 86 | "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion), | ||
| 80 | }, | 87 | }, |
| 81 | resolve: { | 88 | resolve: { |
| 82 | tsconfigPaths: true, | 89 | tsconfigPaths: true, |
bun.lock+29| ... | @@ -124,6 +124,7 @@ | ... | @@ -124,6 +124,7 @@ |
| 124 | "@types/babel__core": "^7.20.5", | 124 | "@types/babel__core": "^7.20.5", |
| 125 | "@types/react": "^19.0.0", | 125 | "@types/react": "^19.0.0", |
| 126 | "@types/react-dom": "^19.0.0", | 126 | "@types/react-dom": "^19.0.0", |
| 127 | "@vercel/config": "^0.3.0", | ||
| 127 | "@vitejs/plugin-react": "^6.0.0", | 128 | "@vitejs/plugin-react": "^6.0.0", |
| 128 | "@vitest/browser-playwright": "^4.0.18", | 129 | "@vitest/browser-playwright": "^4.0.18", |
| 129 | "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", | 130 | "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", |
| ... | @@ -927,6 +928,14 @@ | ... | @@ -927,6 +928,14 @@ |
| 927 | 928 | ||
| 928 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], | 929 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], |
| 929 | 930 | ||
| 931 | "@vercel/config": ["@vercel/config@0.3.0", "", { "dependencies": { "@vercel/routing-utils": "6.2.0", "pretty-cache-header": "^1.0.0", "zod": "^3.22.0" }, "bin": { "config": "dist/cli.js" } }, "sha512-Tf5k5y2F478oTiQcU5R8Ntix1UejE6NdduZnI7aa1XXxjCtifX1XdRS/D2uTjiQAwIL3pLa1LSAN80ABbba+TQ=="], | ||
| 932 | |||
| 933 | "@vercel/functions": ["@vercel/functions@3.5.0", "", { "dependencies": { "@vercel/oidc": "3.4.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity"] }, "sha512-+RokZ+4gkYyOsKBuJ29cQ8iSZG123LLJbZfPry20kkTgrN9U0277La4feP4DnWVo3sGoYa4plCEKY9XKUYoX9g=="], | ||
| 934 | |||
| 935 | "@vercel/oidc": ["@vercel/oidc@3.4.0", "", {}, "sha512-p0sKfHkfRmMaqqDwNL4tjnX9TgRrLMlEtUjIxfrEns8pOxz1R9ztqOVI+ehqiq93/2/HnfPe/UBZkfAZwnx0UA=="], | ||
| 936 | |||
| 937 | "@vercel/routing-utils": ["@vercel/routing-utils@6.2.0", "", { "dependencies": { "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0" }, "optionalDependencies": { "ajv": "^6.12.3" } }, "sha512-YI2cGYZmJKEyGSEgf5Fw5rVuW7X1bTOQ7/pnLRNTFMH3YB3qqP5erCLCJGv2kIvNo1iQAPINHQRJj6ykEdGoPg=="], | ||
| 938 | |||
| 930 | "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "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-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], | 939 | "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "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-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], |
| 931 | 940 | ||
| 932 | "@vitest/browser": ["@vitest/browser@4.1.0", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-tG/iOrgbiHQks0ew7CdelUyNEHkv8NLrt+CqdTivIuoSnXvO7scWMn4Kqo78/UGY1NJ6Hv+vp8BvRnED/bjFdQ=="], | 941 | "@vitest/browser": ["@vitest/browser@4.1.0", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-tG/iOrgbiHQks0ew7CdelUyNEHkv8NLrt+CqdTivIuoSnXvO7scWMn4Kqo78/UGY1NJ6Hv+vp8BvRnED/bjFdQ=="], |
| ... | @@ -1241,6 +1250,8 @@ | ... | @@ -1241,6 +1250,8 @@ |
| 1241 | 1250 | ||
| 1242 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], | 1251 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], |
| 1243 | 1252 | ||
| 1253 | "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], | ||
| 1254 | |||
| 1244 | "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], | 1255 | "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], |
| 1245 | 1256 | ||
| 1246 | "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], | 1257 | "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], |
| ... | @@ -1685,6 +1696,8 @@ | ... | @@ -1685,6 +1696,8 @@ |
| 1685 | 1696 | ||
| 1686 | "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], | 1697 | "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], |
| 1687 | 1698 | ||
| 1699 | "path-to-regexp-updated": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], | ||
| 1700 | |||
| 1688 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], | 1701 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], |
| 1689 | 1702 | ||
| 1690 | "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], | 1703 | "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], |
| ... | @@ -1707,6 +1720,8 @@ | ... | @@ -1707,6 +1720,8 @@ |
| 1707 | 1720 | ||
| 1708 | "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], | 1721 | "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], |
| 1709 | 1722 | ||
| 1723 | "pretty-cache-header": ["pretty-cache-header@1.0.0", "", { "dependencies": { "timestring": "^6.0.0" } }, "sha512-xtXazslu25CdnGnUkByU1RoOjK55TqwatJkjjJLg5ZAdz2Lngko/mmaUgeET36P2GMlNwh3fdM7FWBO717pNcw=="], | ||
| 1724 | |||
| 1710 | "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], | 1725 | "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], |
| 1711 | 1726 | ||
| 1712 | "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], | 1727 | "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], |
| ... | @@ -1717,6 +1732,8 @@ | ... | @@ -1717,6 +1732,8 @@ |
| 1717 | 1732 | ||
| 1718 | "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], | 1733 | "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], |
| 1719 | 1734 | ||
| 1735 | "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], | ||
| 1736 | |||
| 1720 | "pure-rand": ["pure-rand@8.1.0", "", {}, "sha512-53B3MB8wetRdD6JZ4W/0gDKaOvKwuXrEmV1auQc0hASWge8rieKV4PCCVNVbJ+i24miiubb4c/B+dg8Ho0ikYw=="], | 1737 | "pure-rand": ["pure-rand@8.1.0", "", {}, "sha512-53B3MB8wetRdD6JZ4W/0gDKaOvKwuXrEmV1auQc0hASWge8rieKV4PCCVNVbJ+i24miiubb4c/B+dg8Ho0ikYw=="], |
| 1721 | 1738 | ||
| 1722 | "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], | 1739 | "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], |
| ... | @@ -1897,6 +1914,8 @@ | ... | @@ -1897,6 +1914,8 @@ |
| 1897 | 1914 | ||
| 1898 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], | 1915 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], |
| 1899 | 1916 | ||
| 1917 | "timestring": ["timestring@6.0.0", "", {}, "sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA=="], | ||
| 1918 | |||
| 1900 | "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], | 1919 | "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], |
| 1901 | 1920 | ||
| 1902 | "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], | 1921 | "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], |
| ... | @@ -2019,6 +2038,8 @@ | ... | @@ -2019,6 +2038,8 @@ |
| 2019 | 2038 | ||
| 2020 | "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=="], | 2039 | "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=="], |
| 2021 | 2040 | ||
| 2041 | "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], | ||
| 2042 | |||
| 2022 | "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=="], | 2043 | "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=="], |
| 2023 | 2044 | ||
| 2024 | "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], | 2045 | "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], |
| ... | @@ -2193,6 +2214,12 @@ | ... | @@ -2193,6 +2214,12 @@ |
| 2193 | 2214 | ||
| 2194 | "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], | 2215 | "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], |
| 2195 | 2216 | ||
| 2217 | "@vercel/config/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], | ||
| 2218 | |||
| 2219 | "@vercel/routing-utils/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=="], | ||
| 2220 | |||
| 2221 | "@vercel/routing-utils/path-to-regexp": ["path-to-regexp@6.1.0", "", {}, "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="], | ||
| 2222 | |||
| 2196 | "@vitest/browser/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], | 2223 | "@vitest/browser/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], |
| 2197 | 2224 | ||
| 2198 | "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | 2225 | "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
| ... | @@ -2317,6 +2344,8 @@ | ... | @@ -2317,6 +2344,8 @@ |
| 2317 | 2344 | ||
| 2318 | "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], | 2345 | "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], |
| 2319 | 2346 | ||
| 2347 | "@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], | ||
| 2348 | |||
| 2320 | "ast-kit/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.2", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.2", "@babel/helper-validator-identifier": "^8.0.0-rc.2" } }, "sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw=="], | 2349 | "ast-kit/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.2", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.2", "@babel/helper-validator-identifier": "^8.0.0-rc.2" } }, "sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw=="], |
| 2321 | 2350 | ||
| 2322 | "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], | 2351 | "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], |
docs/release.md+53| ... | @@ -24,8 +24,61 @@ This document covers the unified release workflow for stable and nightly desktop | ... | @@ -24,8 +24,61 @@ This document covers the unified release workflow for stable and nightly desktop |
| 24 | - Publishes the CLI package (`apps/server`, npm package `t3`) with OIDC trusted publishing from the same workflow file: | 24 | - Publishes the CLI package (`apps/server`, npm package `t3`) with OIDC trusted publishing from the same workflow file: |
| 25 | - stable releases publish npm dist-tag `latest` | 25 | - stable releases publish npm dist-tag `latest` |
| 26 | - nightly releases publish npm dist-tag `nightly` | 26 | - nightly releases publish npm dist-tag `nightly` |
| 27 | - Deploys the hosted web app to Vercel only after a release is published: | ||
| 28 | - stable releases are aliased to the `latest` hosted app channel | ||
| 29 | - nightly releases are aliased to the `nightly` hosted app channel | ||
| 27 | - Signing is optional and auto-detected per platform from secrets. | 30 | - Signing is optional and auto-detected per platform from secrets. |
| 28 | 31 | ||
| 32 | ## Hosted web app release deployment | ||
| 33 | |||
| 34 | The hosted app is intentionally not deployed by Vercel's Git integration. The | ||
| 35 | web project disables automatic Git deployments in `apps/web/vercel.ts` via | ||
| 36 | `git.deploymentEnabled: false`, and `.github/workflows/release.yml` deploys the | ||
| 37 | web app with Vercel CLI after the GitHub Release succeeds. | ||
| 38 | |||
| 39 | Required GitHub Actions secrets: | ||
| 40 | |||
| 41 | - `VERCEL_TOKEN` | ||
| 42 | - `VERCEL_ORG_ID` | ||
| 43 | - `VERCEL_PROJECT_ID` | ||
| 44 | |||
| 45 | Optional GitHub Actions variables: | ||
| 46 | |||
| 47 | - `VERCEL_TEAM_SLUG`: required only if the token needs an explicit Vercel team scope. | ||
| 48 | - `T3CODE_WEB_ROUTER_URL`: defaults to `https://app.t3.codes`. | ||
| 49 | - `T3CODE_WEB_LATEST_DOMAIN`: defaults to `latest.app.t3.codes`. | ||
| 50 | - `T3CODE_WEB_NIGHTLY_DOMAIN`: defaults to `nightly.app.t3.codes`. | ||
| 51 | |||
| 52 | Required Vercel domains: | ||
| 53 | |||
| 54 | - `app.t3.codes`: the stable router domain users open. | ||
| 55 | - `latest.app.t3.codes`: channel alias updated by stable releases. | ||
| 56 | - `nightly.app.t3.codes`: channel alias updated by nightly releases. | ||
| 57 | |||
| 58 | The router domain uses `apps/web/vercel.ts` routes. Users opt into a channel by | ||
| 59 | visiting `/__t3code/channel?channel=latest` or | ||
| 60 | `/__t3code/channel?channel=nightly`; the router stores the | ||
| 61 | `t3code_web_channel` cookie and rewrites future requests on `app.t3.codes` to | ||
| 62 | the matching channel alias. | ||
| 63 | |||
| 64 | The release deploy job rewrites release package versions before upload so the | ||
| 65 | hosted app's About panel renders the release version. It also passes | ||
| 66 | `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted update track | ||
| 67 | selector in the About panel. Changing the selector navigates through | ||
| 68 | `/__t3code/channel` on the router domain so the user's channel cookie is updated | ||
| 69 | before redirecting to the hosted app root. | ||
| 70 | |||
| 71 | One-time Vercel dashboard setup: | ||
| 72 | |||
| 73 | 1. Confirm the web project root directory remains `apps/web`. | ||
| 74 | 2. Add the three domains above to the web project. | ||
| 75 | 3. Disable automatic Git deployments in the dashboard if desired; the committed | ||
| 76 | `vercel.ts` setting is the source-of-truth, but disconnecting Git in the | ||
| 77 | dashboard is also safe. | ||
| 78 | 4. Promote or alias one deployment containing the router rules in `apps/web/vercel.ts` to | ||
| 79 | `app.t3.codes` once. Future release jobs should only update the channel | ||
| 80 | aliases. | ||
| 81 | |||
| 29 | ## Nightly builds | 82 | ## Nightly builds |
| 30 | 83 | ||
| 31 | - Workflow: `.github/workflows/release.yml` | 84 | - Workflow: `.github/workflows/release.yml` |