feat(branding): brand kit architecture (Phase 1) #96

Merged
padreug merged 10 commits from feat/brand-kit into dev 2026-06-10 08:17:56 +00:00
10 changed files with 45 additions and 9 deletions
Showing only changes of commit 3efae30e84 - Show all commits

feat(branding): auto-generate icons on vite build/dev start

vite-branding.ts now exports brandAssetsPlugin() — a Vite plugin
whose buildStart hook runs scripts/generate-pwa-assets.mjs once per
build / dev-server start. All 9 vite configs register it first in
plugins[], so PWA icons under public/icons/ are guaranteed to exist
before VitePWA's includeAssets / manifest.icons get processed and
before the public/ → dist/ copy.

Removes the "did you remember to pnpm generate-pwa-assets?" failure
mode. Dev mode now also auto-populates icons (no more dev 404s on
/icons/favicon.ico).

Verified build from clean state (no public/icons/ pre-existing): the
plugin generates, all 6 icons land in dist-wallet/icons/.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Padreug 2026-06-09 23:43:50 +02:00

View file

@ -1,5 +1,7 @@
import { spawnSync } from 'node:child_process'
import { readFileSync } from 'node:fs' import { readFileSync } from 'node:fs'
import { resolve } from 'node:path' import { resolve } from 'node:path'
import type { Plugin } from 'vite'
/** /**
* Absolute path to the active brand kit. Deployers point this at their * Absolute path to the active brand kit. Deployers point this at their
@ -51,3 +53,28 @@ export const brandAlias = {
export function brandManifestName(appLabel?: string): string { export function brandManifestName(appLabel?: string): string {
return appLabel ? `${brand.name} ${appLabel}` : brand.name return appLabel ? `${brand.name} ${appLabel}` : brand.name
} }
/**
* Vite plugin: regenerate PWA icons under public/icons/ once per build
* / dev-server start, so vite.<app>.config.ts's includeAssets +
* manifest.icons always have something to include. Source resolution
* lives in pwa-assets.config.ts.
*/
export function brandAssetsPlugin(): Plugin {
let generated = false
return {
name: 'brand-assets-generator',
buildStart() {
if (generated) return
const { status } = spawnSync(
'node',
[resolve('scripts/generate-pwa-assets.mjs')],
{ stdio: 'inherit' },
)
if (status !== 0) {
throw new Error('pwa-assets-generator failed; see output above')
}
generated = true
},
}
}

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
function chatHtmlPlugin(): Plugin { function chatHtmlPlugin(): Plugin {
return { return {
@ -46,6 +46,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
chatHtmlPlugin(), chatHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),

View file

@ -5,7 +5,7 @@ import { defineConfig } from 'vite'
import Inspect from 'vite-plugin-inspect' import Inspect from 'vite-plugin-inspect'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
// https://vite.dev/config/ // https://vite.dev/config/
// //
@ -26,6 +26,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),
Inspect(), Inspect(),

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias, brandManifestName } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin, brandManifestName } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to events.html * Plugin to rewrite dev server requests to events.html
@ -58,6 +58,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
eventsHtmlPlugin(), eventsHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
function forumHtmlPlugin(): Plugin { function forumHtmlPlugin(): Plugin {
return { return {
@ -46,6 +46,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
forumHtmlPlugin(), forumHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to libra.html * Plugin to rewrite dev server requests to libra.html
@ -51,6 +51,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
libraHtmlPlugin(), libraHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
function marketHtmlPlugin(): Plugin { function marketHtmlPlugin(): Plugin {
return { return {
@ -46,6 +46,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
marketHtmlPlugin(), marketHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
function restaurantHtmlPlugin(): Plugin { function restaurantHtmlPlugin(): Plugin {
return { return {
@ -51,6 +51,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
restaurantHtmlPlugin(), restaurantHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
function tasksHtmlPlugin(): Plugin { function tasksHtmlPlugin(): Plugin {
return { return {
@ -46,6 +46,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
tasksHtmlPlugin(), tasksHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),

View file

@ -5,7 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brand, brandAlias } from './vite-branding' import { brand, brandAlias, brandAssetsPlugin } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to wallet.html * Plugin to rewrite dev server requests to wallet.html
@ -50,6 +50,7 @@ export default defineConfig(({ mode }) => ({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
brandAssetsPlugin(),
walletHtmlPlugin(), walletHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),