From 1a73926e5f482893883d0602974210ac49d26d17 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Thu, 23 Jul 2026 13:44:34 -0400 Subject: [PATCH] fix(cli): publish nightly branded favicons --- apps/server/scripts/cli.ts | 97 ++++++++++++++------------------ scripts/lib/brand-assets.test.ts | 11 +++- scripts/lib/brand-assets.ts | 6 +- 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index 00b6c4cfcce..517f633577c 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -12,7 +12,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { DEVELOPMENT_ICON_OVERRIDES, - PUBLISH_ICON_OVERRIDES, + resolveWebAssetBrandForPackageVersion, + resolveWebIconOverrides, } from "../../../scripts/lib/brand-assets.ts"; import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts"; import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; @@ -81,50 +82,34 @@ const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Stan } }); -interface PublishIconBackup { - readonly targetPath: string; - readonly backupPath: string; -} - -const applyPublishIconOverrides = Effect.fn("applyPublishIconOverrides")(function* ( +const preparePublishIcons = Effect.fn("preparePublishIcons")(function* ( repoRoot: string, serverDir: string, + version: string, ) { const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; - const backups: PublishIconBackup[] = []; - - for (const override of PUBLISH_ICON_OVERRIDES) { - const sourcePath = path.join(repoRoot, override.sourceRelativePath); - const targetPath = path.join(serverDir, override.targetRelativePath); - const backupPath = `${targetPath}.publish-bak`; - - if (!(yield* fs.exists(sourcePath))) { - return yield* new ServerCliPublishIconSourceMissingError({ sourcePath }); + const brand = resolveWebAssetBrandForPackageVersion(version); + const icons = resolveWebIconOverrides(brand, "dist/client").map((override) => ({ + sourcePath: path.join(repoRoot, override.sourceRelativePath), + targetPath: path.join(serverDir, override.targetRelativePath), + })); + + for (const icon of icons) { + if (!(yield* fs.exists(icon.sourcePath))) { + return yield* new ServerCliPublishIconSourceMissingError({ sourcePath: icon.sourcePath }); } - if (!(yield* fs.exists(targetPath))) { - return yield* new ServerCliPublishIconTargetMissingError({ targetPath }); + if (!(yield* fs.exists(icon.targetPath))) { + return yield* new ServerCliPublishIconTargetMissingError({ targetPath: icon.targetPath }); } - - yield* fs.copyFile(targetPath, backupPath); - yield* fs.copyFile(sourcePath, targetPath); - backups.push({ targetPath, backupPath }); } - yield* Effect.log("[cli] Applied publish icon overrides to dist/client"); - return backups as ReadonlyArray; -}); - -const restorePublishIconOverrides = Effect.fn("restorePublishIconOverrides")(function* ( - backups: ReadonlyArray, -) { - const fs = yield* FileSystem.FileSystem; - for (const backup of backups) { - if (!(yield* fs.exists(backup.backupPath))) { - continue; - } - yield* fs.rename(backup.backupPath, backup.targetPath); - } + return yield* Effect.forEach(icons, (icon) => + Effect.all({ + original: fs.readFile(icon.targetPath), + publish: fs.readFile(icon.sourcePath), + }).pipe(Effect.map((contents) => ({ ...icon, ...contents }))), + ); }); const applyDevelopmentIconOverrides = Effect.fn("applyDevelopmentIconOverrides")(function* ( @@ -236,7 +221,6 @@ const publishCmd = Command.make( const repoRoot = yield* RepoRoot; const serverDir = path.join(repoRoot, "apps/server"); const packageJsonPath = path.join(serverDir, "package.json"); - const backupPath = `${packageJsonPath}.bak`; // Assert build assets exist for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) { @@ -247,7 +231,7 @@ const publishCmd = Command.make( } yield* Effect.acquireUseRelease( - // Acquire: backup package.json, resolve catalog dependencies, and strip devDependencies/scripts + // Acquire: resolve publish metadata and read every original before mutation. Effect.gen(function* () { const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version); const workspaceConfig = yield* readWorkspaceConfig(); @@ -273,19 +257,22 @@ const publishCmd = Command.make( ), }; - const original = yield* fs.readFileString(packageJsonPath); - const packageJsonString = yield* encodePackageJson(pkg); - yield* fs.writeFileString(backupPath, original); - yield* fs.writeFileString(packageJsonPath, `${packageJsonString}\n`); - yield* Effect.log("[cli] Prepared package.json for publish"); - - const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir); - return { iconBackups }; + return { + packageJsonString: yield* encodePackageJson(pkg), + originalPackageJson: yield* fs.readFile(packageJsonPath), + icons: yield* preparePublishIcons(repoRoot, serverDir, version), + }; }), // Use: pnpm publish from the workspace root so pnpm-only workspace // config, including override selectors, is interpreted correctly. - () => + (resource) => Effect.gen(function* () { + yield* fs.writeFileString(packageJsonPath, `${resource.packageJsonString}\n`); + for (const icon of resource.icons) { + yield* fs.writeFile(icon.targetPath, icon.publish); + } + yield* Effect.log("[cli] Applied package metadata and publish icon overrides"); + const args = createVpPmPublishArgs(config); const spawnCommand = yield* resolveSpawnCommand("vp", ["pm", ...args]); @@ -299,16 +286,14 @@ const publishCmd = Command.make( }), ); }), - // Release: restore - (resource: { readonly iconBackups: ReadonlyArray }) => + // Release: restore every file even if applying overrides or publishing fails. + (resource) => Effect.gen(function* () { - yield* restorePublishIconOverrides(resource.iconBackups).pipe( - Effect.catch((error) => - Effect.logError(`[cli] Failed to restore publish icon overrides: ${String(error)}`), - ), - ); - yield* fs.rename(backupPath, packageJsonPath); - if (config.verbose) yield* Effect.log("[cli] Restored original package.json"); + yield* fs.writeFile(packageJsonPath, resource.originalPackageJson); + for (const icon of resource.icons) { + yield* fs.writeFile(icon.targetPath, icon.original); + } + if (config.verbose) yield* Effect.log("[cli] Restored original publish assets"); }), ); }), diff --git a/scripts/lib/brand-assets.test.ts b/scripts/lib/brand-assets.test.ts index c830f3d1c64..8265ad68180 100644 --- a/scripts/lib/brand-assets.test.ts +++ b/scripts/lib/brand-assets.test.ts @@ -4,14 +4,14 @@ import { BRAND_ASSET_PATHS, DEVELOPMENT_ICON_OVERRIDES, DEVELOPMENT_PUBLIC_ICON_OVERRIDES, - PUBLISH_ICON_OVERRIDES, resolveWebAssetBrandForChannel, + resolveWebAssetBrandForPackageVersion, resolveWebIconOverrides, } from "./brand-assets.ts"; describe("brand-assets", () => { - it("maps server publish web assets to production icons", () => { - expect(PUBLISH_ICON_OVERRIDES).toEqual([ + it("maps production web assets into the server package", () => { + expect(resolveWebIconOverrides("production", "dist/client")).toEqual([ { sourceRelativePath: BRAND_ASSET_PATHS.productionWebFaviconIco, targetRelativePath: "dist/client/favicon.ico", @@ -78,6 +78,11 @@ describe("brand-assets", () => { expect(resolveWebAssetBrandForChannel("nightly")).toBe("nightly"); }); + it("maps package versions to web asset brands", () => { + expect(resolveWebAssetBrandForPackageVersion("0.0.29")).toBe("production"); + expect(resolveWebAssetBrandForPackageVersion("0.0.29-nightly.20260723.882")).toBe("nightly"); + }); + it("keeps development, nightly, and production icon families separate", () => { expect([ BRAND_ASSET_PATHS.developmentIconComposerProject, diff --git a/scripts/lib/brand-assets.ts b/scripts/lib/brand-assets.ts index 9ab72c2cd75..2dcc6ccd6ce 100644 --- a/scripts/lib/brand-assets.ts +++ b/scripts/lib/brand-assets.ts @@ -41,6 +41,10 @@ export function resolveWebAssetBrandForChannel(channel: WebAssetChannel): WebAss return channel === "nightly" ? "nightly" : "production"; } +export function resolveWebAssetBrandForPackageVersion(version: string): WebAssetBrand { + return version.includes("-nightly.") ? "nightly" : "production"; +} + export interface IconOverride { readonly sourceRelativePath: string; readonly targetRelativePath: string; @@ -105,5 +109,3 @@ export const DEVELOPMENT_PUBLIC_ICON_OVERRIDES = resolveWebIconOverrides( "development", "apps/web/public", ); - -export const PUBLISH_ICON_OVERRIDES = resolveWebIconOverrides("production", "dist/client");