authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-08 22:57:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-08 22:57:16-07:00
log3c32bc8fd1f5970e65988e36937cb8e2921437f9
treec24c888016ba4469a7d38dd014fad5553c6aa62b
parent6efdf67e157a3c5e88b120dcf6857176534d4aa8
signature Signed by PGP key B5690EEEBB952194

[codex] Fix hosted channel bootstrap (#2613)

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", () => {
7979 vi.stubEnv("VITE_HTTP_URL", "https://backend.example.com");
8080 expect(isHostedStaticApp(new URL("https://preview.t3.codes/"))).toBe(false);
8181 });
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 });
8294});
apps/web/src/hostedPairing.ts+9
......@@ -18,6 +18,11 @@ function configuredBackendUrl(): string {
1818 return import.meta.env.VITE_HTTP_URL?.trim() || import.meta.env.VITE_WS_URL?.trim() || "";
1919}
2020
21function 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
2126function originFromUrl(value: string): string | null {
2227 try {
2328 return new URL(value).origin;
......@@ -31,6 +36,10 @@ export function isHostedStaticApp(url: URL = new URL(window.location.href)): boo
3136 return false;
3237 }
3338
39 if (configuredHostedAppChannel()) {
40 return true;
41 }
42
3443 const hostedOrigin = originFromUrl(configuredHostedAppUrl());
3544 return hostedOrigin !== null && url.origin === hostedOrigin;
3645}
apps/web/vercel.ts+1-1
......@@ -18,7 +18,7 @@ function channelCookie(channel: "latest" | "nightly"): string {
1818
1919export const config: VercelConfig = {
2020 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}"',
2222 git: {
2323 deploymentEnabled: false,
2424 },
scripts/apply-web-brand-assets.ts+20-4
......@@ -6,11 +6,17 @@ import * as Effect from "effect/Effect";
66import * as FileSystem from "effect/FileSystem";
77import * as Option from "effect/Option";
88import * as Path from "effect/Path";
9import { Argument, Command } from "effect/unstable/cli";
10import { resolveWebIconOverrides, type WebAssetBrand } from "./lib/brand-assets.ts";
9import { Argument, Command, Flag } from "effect/unstable/cli";
10import {
11 resolveWebAssetBrandForChannel,
12 resolveWebIconOverrides,
13 WEB_ASSET_CHANNELS,
14 type WebAssetBrand,
15} from "./lib/brand-assets.ts";
1116
1217const WEB_ASSET_BRANDS = [
1318 "development",
19 "nightly",
1420 "production",
1521] as const satisfies ReadonlyArray<WebAssetBrand>;
1622
......@@ -38,15 +44,25 @@ export const applyWebBrandAssetsCommand = Command.make(
3844 {
3945 brand: Argument.choice("brand", WEB_ASSET_BRANDS).pipe(
4046 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,
4152 ),
4253 targetDirectory: Argument.string("target-directory").pipe(
4354 Argument.withDescription("Output directory that contains the hosted web build assets."),
4455 Argument.optional,
4556 ),
4657 },
47 ({ brand, targetDirectory }) =>
58 ({ brand, channel, targetDirectory }) =>
4859 applyWebBrandAssets(
49 brand,
60 Option.getOrElse(brand, () =>
61 Option.match(channel, {
62 onNone: () => "production" as const,
63 onSome: resolveWebAssetBrandForChannel,
64 }),
65 ),
5066 Option.getOrElse(targetDirectory, () => "apps/web/dist"),
5167 ),
5268).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 {
44 BRAND_ASSET_PATHS,
55 DEVELOPMENT_ICON_OVERRIDES,
66 PUBLISH_ICON_OVERRIDES,
7 resolveWebAssetBrandForChannel,
78 resolveWebIconOverrides,
89} from "./brand-assets.ts";
910
......@@ -42,4 +43,16 @@ describe("brand-assets", () => {
4243 targetRelativePath: "apps/web/dist/apple-touch-icon.png",
4344 });
4445 });
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 });
4558});
scripts/lib/brand-assets.ts+19-1
......@@ -10,6 +10,10 @@ export const BRAND_ASSET_PATHS = {
1010 nightlyMacIconPng: "assets/nightly/blueprint-macos-1024.png",
1111 nightlyLinuxIconPng: "assets/nightly/blueprint-universal-1024.png",
1212 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",
1317
1418 developmentDesktopIconPng: "assets/dev/blueprint-macos-1024.png",
1519 developmentWindowsIconIco: "assets/dev/blueprint-windows.ico",
......@@ -19,7 +23,15 @@ export const BRAND_ASSET_PATHS = {
1923 developmentWebAppleTouchIconPng: "assets/dev/blueprint-web-apple-touch-180.png",
2024} as const;
2125
22export type WebAssetBrand = "development" | "production";
26export type WebAssetBrand = "development" | "nightly" | "production";
27
28export const WEB_ASSET_CHANNELS = ["latest", "nightly"] as const;
29
30export type WebAssetChannel = (typeof WEB_ASSET_CHANNELS)[number];
31
32export function resolveWebAssetBrandForChannel(channel: WebAssetChannel): WebAssetBrand {
33 return channel === "nightly" ? "nightly" : "production";
34}
2335
2436export interface IconOverride {
2537 readonly sourceRelativePath: string;
......@@ -40,6 +52,12 @@ const WEB_ICON_SOURCE_PATHS_BY_BRAND = {
4052 favicon32Png: BRAND_ASSET_PATHS.developmentWebFavicon32Png,
4153 appleTouchIconPng: BRAND_ASSET_PATHS.developmentWebAppleTouchIconPng,
4254 },
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 },
4361 production: {
4462 faviconIco: BRAND_ASSET_PATHS.productionWebFaviconIco,
4563 favicon16Png: BRAND_ASSET_PATHS.productionWebFavicon16Png,