| author | |
| committer | |
| log | 3c32bc8fd1f5970e65988e36937cb8e2921437f9 |
| tree | c24c888016ba4469a7d38dd014fad5553c6aa62b |
| parent | 6efdf67e157a3c5e88b120dcf6857176534d4aa8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius@macmini.local>6 files changed, 74 insertions(+), 6 deletions(-)
apps/web/src/hostedPairing.test.ts+12| ... | ... | @@ -79,4 +79,16 @@ describe("hostedPairing", () => { |
| 79 | 79 | vi.stubEnv("VITE_HTTP_URL", "https://backend.example.com"); |
| 80 | 80 | expect(isHostedStaticApp(new URL("https://preview.t3.codes/"))).toBe(false); |
| 81 | 81 | }); |
| 82 | ||
| 83 | it("detects hosted channel aliases as static apps", () => { | |
| 84 | vi.stubEnv("VITE_HOSTED_APP_URL", "https://app.t3.codes"); | |
| 85 | vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "nightly"); | |
| 86 | vi.stubEnv("VITE_HTTP_URL", ""); | |
| 87 | vi.stubEnv("VITE_WS_URL", ""); | |
| 88 | ||
| 89 | expect(isHostedStaticApp(new URL("https://nightly.app.t3.codes/"))).toBe(true); | |
| 90 | ||
| 91 | vi.stubEnv("VITE_HTTP_URL", "https://backend.example.com"); | |
| 92 | expect(isHostedStaticApp(new URL("https://nightly.app.t3.codes/"))).toBe(false); | |
| 93 | }); | |
| 82 | 94 | }); |
apps/web/src/hostedPairing.ts+9| ... | ... | @@ -18,6 +18,11 @@ function configuredBackendUrl(): string { |
| 18 | 18 | return import.meta.env.VITE_HTTP_URL?.trim() || import.meta.env.VITE_WS_URL?.trim() || ""; |
| 19 | 19 | } |
| 20 | 20 | |
| 21 | function configuredHostedAppChannel(): HostedAppChannel | null { | |
| 22 | const channel = import.meta.env.VITE_HOSTED_APP_CHANNEL?.trim().toLowerCase(); | |
| 23 | return channel === "latest" || channel === "nightly" ? channel : null; | |
| 24 | } | |
| 25 | ||
| 21 | 26 | function originFromUrl(value: string): string | null { |
| 22 | 27 | try { |
| 23 | 28 | return new URL(value).origin; |
| ... | ... | @@ -31,6 +36,10 @@ export function isHostedStaticApp(url: URL = new URL(window.location.href)): boo |
| 31 | 36 | return false; |
| 32 | 37 | } |
| 33 | 38 | |
| 39 | if (configuredHostedAppChannel()) { | |
| 40 | return true; | |
| 41 | } | |
| 42 | ||
| 34 | 43 | const hostedOrigin = originFromUrl(configuredHostedAppUrl()); |
| 35 | 44 | return hostedOrigin !== null && url.origin === hostedOrigin; |
| 36 | 45 | } |
apps/web/vercel.ts+1-1| ... | ... | @@ -18,7 +18,7 @@ function channelCookie(channel: "latest" | "nightly"): string { |
| 18 | 18 | |
| 19 | 19 | export const config: VercelConfig = { |
| 20 | 20 | buildCommand: |
| 21 | "turbo build --filter @t3tools/web && bun ../../scripts/apply-web-brand-assets.ts production", | |
| 21 | 'turbo build --filter @t3tools/web && bun ../../scripts/apply-web-brand-assets.ts --channel "${VITE_HOSTED_APP_CHANNEL:-latest}"', | |
| 22 | 22 | git: { |
| 23 | 23 | deploymentEnabled: false, |
| 24 | 24 | }, |
scripts/apply-web-brand-assets.ts+20-4| ... | ... | @@ -6,11 +6,17 @@ import * as Effect from "effect/Effect"; |
| 6 | 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | 7 | import * as Option from "effect/Option"; |
| 8 | 8 | import * as Path from "effect/Path"; |
| 9 | import { Argument, Command } from "effect/unstable/cli"; | |
| 10 | import { resolveWebIconOverrides, type WebAssetBrand } from "./lib/brand-assets.ts"; | |
| 9 | import { Argument, Command, Flag } from "effect/unstable/cli"; | |
| 10 | import { | |
| 11 | resolveWebAssetBrandForChannel, | |
| 12 | resolveWebIconOverrides, | |
| 13 | WEB_ASSET_CHANNELS, | |
| 14 | type WebAssetBrand, | |
| 15 | } from "./lib/brand-assets.ts"; | |
| 11 | 16 | |
| 12 | 17 | const WEB_ASSET_BRANDS = [ |
| 13 | 18 | "development", |
| 19 | "nightly", | |
| 14 | 20 | "production", |
| 15 | 21 | ] as const satisfies ReadonlyArray<WebAssetBrand>; |
| 16 | 22 | |
| ... | ... | @@ -38,15 +44,25 @@ export const applyWebBrandAssetsCommand = Command.make( |
| 38 | 44 | { |
| 39 | 45 | brand: Argument.choice("brand", WEB_ASSET_BRANDS).pipe( |
| 40 | 46 | Argument.withDescription("Asset brand to copy into the hosted web output directory."), |
| 47 | Argument.optional, | |
| 48 | ), | |
| 49 | channel: Flag.choice("channel", WEB_ASSET_CHANNELS).pipe( | |
| 50 | Flag.withDescription("Hosted release channel to map to a web asset brand."), | |
| 51 | Flag.optional, | |
| 41 | 52 | ), |
| 42 | 53 | targetDirectory: Argument.string("target-directory").pipe( |
| 43 | 54 | Argument.withDescription("Output directory that contains the hosted web build assets."), |
| 44 | 55 | Argument.optional, |
| 45 | 56 | ), |
| 46 | 57 | }, |
| 47 | ({ brand, targetDirectory }) => | |
| 58 | ({ brand, channel, targetDirectory }) => | |
| 48 | 59 | applyWebBrandAssets( |
| 49 | brand, | |
| 60 | Option.getOrElse(brand, () => | |
| 61 | Option.match(channel, { | |
| 62 | onNone: () => "production" as const, | |
| 63 | onSome: resolveWebAssetBrandForChannel, | |
| 64 | }), | |
| 65 | ), | |
| 50 | 66 | Option.getOrElse(targetDirectory, () => "apps/web/dist"), |
| 51 | 67 | ), |
| 52 | 68 | ).pipe(Command.withDescription("Copy web brand assets into a built hosted web app.")); |
scripts/lib/brand-assets.test.ts+13| ... | ... | @@ -4,6 +4,7 @@ import { |
| 4 | 4 | BRAND_ASSET_PATHS, |
| 5 | 5 | DEVELOPMENT_ICON_OVERRIDES, |
| 6 | 6 | PUBLISH_ICON_OVERRIDES, |
| 7 | resolveWebAssetBrandForChannel, | |
| 7 | 8 | resolveWebIconOverrides, |
| 8 | 9 | } from "./brand-assets.ts"; |
| 9 | 10 | |
| ... | ... | @@ -42,4 +43,16 @@ describe("brand-assets", () => { |
| 42 | 43 | targetRelativePath: "apps/web/dist/apple-touch-icon.png", |
| 43 | 44 | }); |
| 44 | 45 | }); |
| 46 | ||
| 47 | it("maps hosted nightly web assets to nightly icons", () => { | |
| 48 | expect(resolveWebIconOverrides("nightly", "apps/web/dist")).toContainEqual({ | |
| 49 | sourceRelativePath: BRAND_ASSET_PATHS.nightlyWebFaviconIco, | |
| 50 | targetRelativePath: "apps/web/dist/favicon.ico", | |
| 51 | }); | |
| 52 | }); | |
| 53 | ||
| 54 | it("maps hosted release channels to web asset brands", () => { | |
| 55 | expect(resolveWebAssetBrandForChannel("latest")).toBe("production"); | |
| 56 | expect(resolveWebAssetBrandForChannel("nightly")).toBe("nightly"); | |
| 57 | }); | |
| 45 | 58 | }); |
scripts/lib/brand-assets.ts+19-1| ... | ... | @@ -10,6 +10,10 @@ export const BRAND_ASSET_PATHS = { |
| 10 | 10 | nightlyMacIconPng: "assets/nightly/blueprint-macos-1024.png", |
| 11 | 11 | nightlyLinuxIconPng: "assets/nightly/blueprint-universal-1024.png", |
| 12 | 12 | nightlyWindowsIconIco: "assets/nightly/blueprint-windows.ico", |
| 13 | nightlyWebFaviconIco: "assets/nightly/blueprint-web-favicon.ico", | |
| 14 | nightlyWebFavicon16Png: "assets/nightly/blueprint-web-favicon-16x16.png", | |
| 15 | nightlyWebFavicon32Png: "assets/nightly/blueprint-web-favicon-32x32.png", | |
| 16 | nightlyWebAppleTouchIconPng: "assets/nightly/blueprint-web-apple-touch-180.png", | |
| 13 | 17 | |
| 14 | 18 | developmentDesktopIconPng: "assets/dev/blueprint-macos-1024.png", |
| 15 | 19 | developmentWindowsIconIco: "assets/dev/blueprint-windows.ico", |
| ... | ... | @@ -19,7 +23,15 @@ export const BRAND_ASSET_PATHS = { |
| 19 | 23 | developmentWebAppleTouchIconPng: "assets/dev/blueprint-web-apple-touch-180.png", |
| 20 | 24 | } as const; |
| 21 | 25 | |
| 22 | export type WebAssetBrand = "development" | "production"; | |
| 26 | export type WebAssetBrand = "development" | "nightly" | "production"; | |
| 27 | ||
| 28 | export const WEB_ASSET_CHANNELS = ["latest", "nightly"] as const; | |
| 29 | ||
| 30 | export type WebAssetChannel = (typeof WEB_ASSET_CHANNELS)[number]; | |
| 31 | ||
| 32 | export function resolveWebAssetBrandForChannel(channel: WebAssetChannel): WebAssetBrand { | |
| 33 | return channel === "nightly" ? "nightly" : "production"; | |
| 34 | } | |
| 23 | 35 | |
| 24 | 36 | export interface IconOverride { |
| 25 | 37 | readonly sourceRelativePath: string; |
| ... | ... | @@ -40,6 +52,12 @@ const WEB_ICON_SOURCE_PATHS_BY_BRAND = { |
| 40 | 52 | favicon32Png: BRAND_ASSET_PATHS.developmentWebFavicon32Png, |
| 41 | 53 | appleTouchIconPng: BRAND_ASSET_PATHS.developmentWebAppleTouchIconPng, |
| 42 | 54 | }, |
| 55 | nightly: { | |
| 56 | faviconIco: BRAND_ASSET_PATHS.nightlyWebFaviconIco, | |
| 57 | favicon16Png: BRAND_ASSET_PATHS.nightlyWebFavicon16Png, | |
| 58 | favicon32Png: BRAND_ASSET_PATHS.nightlyWebFavicon32Png, | |
| 59 | appleTouchIconPng: BRAND_ASSET_PATHS.nightlyWebAppleTouchIconPng, | |
| 60 | }, | |
| 43 | 61 | production: { |
| 44 | 62 | faviconIco: BRAND_ASSET_PATHS.productionWebFaviconIco, |
| 45 | 63 | favicon16Png: BRAND_ASSET_PATHS.productionWebFavicon16Png, |