mirror of
https://github.com/luxfi/safe-wallet.git
synced 2026-07-26 22:53:37 +00:00
feat(brand): per-brand build configs and asset pipeline
apps/web/package.json gains build:safe / build:lux / build:hanzo scripts. Each runs scripts/select-brand.sh <slug> (which copies .env.<slug> -> .env.local, brand/<slug>/ -> brand/active/, and the per-brand manifest.json over safe.webmanifest) then `yarn build`. scripts/codemod-brand-urls.mjs is the Pass 1 driver kept in-tree so future merges with upstream can re-run it. .gitignore covers the brand/active output dir. HANZO_WHITELABEL.md updated with the per-brand invocation, the canonical pattern table, the protocol-vs-brand boundary, and how to add a new brand (env + asset dir + one package.json script — no source edits).
This commit is contained in:
@@ -89,6 +89,8 @@ thumbs.db
|
||||
# web
|
||||
apps/web/.next/*
|
||||
apps/web/out/*
|
||||
# white-label build output: select-brand.sh copies the active brand here
|
||||
apps/web/public/brand/active/
|
||||
apps/web/public/work*.js
|
||||
apps/web/public/sw.js
|
||||
apps/web/public/firebase*.js
|
||||
|
||||
+101
-68
@@ -1,97 +1,130 @@
|
||||
# Safe Wallet — White-Label Plan
|
||||
|
||||
This monorepo is a fork of `safe-global/safe-wallet-monorepo`. We want
|
||||
a single codebase that can ship multiple branded wallets (Lux, Hanzo,
|
||||
partner builds) without forking per brand.
|
||||
This monorepo is a fork of `safe-global/safe-wallet-monorepo`. We ship multiple
|
||||
branded wallets (Lux Safe, Hanzo Vault, partner builds) from a single source.
|
||||
|
||||
## The strategy: one source, env-driven brand tokens
|
||||
## Strategy
|
||||
|
||||
Every hardcoded brand reference in app code gets replaced with a
|
||||
read from a single `Brand` config object. Brand config is sourced
|
||||
from env vars at build time, **never** baked into the source files.
|
||||
One source. Env-driven brand tokens. Every hardcoded brand reference in app
|
||||
code reads from a single `Brand` config object exposed by the workspace
|
||||
package `@safe-global/brand`.
|
||||
|
||||
```ts
|
||||
// packages/brand/src/index.ts (new package)
|
||||
// packages/brand/src/index.ts
|
||||
export interface Brand {
|
||||
name: string // "Lux Safe", "Hanzo Vault", …
|
||||
shortName: string // "Safe", "Vault" — used in UI chrome
|
||||
domain: string // "safe.lux.network"
|
||||
email: string // "support@lux.network"
|
||||
logoUrl: string // "/brand/logo.svg" (per-tenant CDN)
|
||||
faviconUrl: string
|
||||
primaryColor: string
|
||||
twitterHandle?: string
|
||||
discordUrl?: string
|
||||
name: string
|
||||
shortName: string
|
||||
domain: string
|
||||
appHost: string
|
||||
appUrl: string
|
||||
email: string
|
||||
helpUrl: string
|
||||
termsUrl: string
|
||||
privacyUrl: string
|
||||
// …
|
||||
// … and more
|
||||
}
|
||||
|
||||
export const brand: Brand = {
|
||||
name: process.env.NEXT_PUBLIC_BRAND_NAME ?? "Safe",
|
||||
shortName: process.env.NEXT_PUBLIC_BRAND_SHORT_NAME ?? "Safe",
|
||||
domain: process.env.NEXT_PUBLIC_BRAND_DOMAIN ?? "safe.global",
|
||||
email: process.env.NEXT_PUBLIC_BRAND_EMAIL ?? "support@safe.global",
|
||||
name: env('NAME', 'Safe{Wallet}'),
|
||||
domain: env('DOMAIN', 'safe.global'),
|
||||
email: env('EMAIL', 'support@safe.global'),
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
Every consumer imports from `@hanzo/safe-brand` (workspace alias)
|
||||
and renders `brand.name` / `brand.logoUrl` / etc. No `'Safe'` string
|
||||
literal anywhere in `apps/` or `packages/*/src/`.
|
||||
Defaults match upstream Safe values, so an unbranded build is byte-for-byte
|
||||
the same as today. Per-brand builds override via `NEXT_PUBLIC_BRAND_*` (web)
|
||||
or `EXPO_PUBLIC_BRAND_*` (mobile) env vars at build time.
|
||||
|
||||
## Brand surface inventory (audit)
|
||||
## Pattern table
|
||||
|
||||
| Pattern | Replacement |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| `'Safe{Wallet}'` / `Safe{Mobile}` | `brand.name` |
|
||||
| `'Safe'` (subject noun in chrome) | `brand.shortName` |
|
||||
| `https://help.safe.global/...` | `${brand.helpUrl}/...` |
|
||||
| `https://app.safe.global/...` | `${brand.appUrl}/...` |
|
||||
| `https://safe.global/{terms,…}` | `brand.{terms,privacy,imprint,…}Url` |
|
||||
| `support@safe.global` | `brand.email` |
|
||||
| `/images/safe-logo*.{png,svg}` | `brand.logoUrl` |
|
||||
| `app.safe.global` (host literal) | `brand.appHost` |
|
||||
| `anon.safe.global` | `brand.supportChatAliasDomain` |
|
||||
| `https://chat.safe.global` | `brand.discordUrl` |
|
||||
| `https://twitter.com/safe` | `brand.twitterUrl` |
|
||||
| `https://status.safe.global` | `brand.statusUrl` |
|
||||
| `https://developer.safe.global` | `brand.developerUrl` |
|
||||
| webmanifest `name`/`description` | per-brand `public/brand/<slug>/manifest.json` |
|
||||
| mobile Expo `name` | reads `EXPO_PUBLIC_BRAND_NAME` |
|
||||
|
||||
## What stays upstream (not brand strings)
|
||||
|
||||
- Smart contract addresses (every brand uses the same Safe contracts).
|
||||
- `safe-client.safe.global` (CGW — Safe Transaction Service host; protocol).
|
||||
- `safe-transaction-*.safe.global` (the per-network tx-service).
|
||||
- `safe-transaction-assets.safe.global` (chain logo CDN).
|
||||
- npm package names of upstream deps (`@safe-global/protocol-kit`,
|
||||
`@safe-global/api-kit`, etc.).
|
||||
- TypeScript identifiers (`SafeInfo`, `useSafeInfo`, `class Safe`,
|
||||
`safe.address` etc.) — these are protocol terms, not brand strings.
|
||||
- `OFFICIAL_HOSTS` / `IPFS_HOSTS` regexes (identity check for the
|
||||
upstream-deployed canonical host).
|
||||
|
||||
## Per-brand build
|
||||
|
||||
Three sample brand bundles ship in-tree:
|
||||
|
||||
| Slug | Brand | Env file | Asset dir |
|
||||
| ------- | ------------ | --------------------- | ------------------------------ |
|
||||
| `safe` | Safe{Wallet} | `apps/web/.env.safe` | `apps/web/public/brand/safe/` |
|
||||
| `lux` | Lux Safe | `apps/web/.env.lux` | `apps/web/public/brand/lux/` |
|
||||
| `hanzo` | Hanzo Vault | `apps/web/.env.hanzo` | `apps/web/public/brand/hanzo/` |
|
||||
|
||||
Each `apps/web/public/brand/<slug>/` directory holds the per-brand
|
||||
`manifest.json` plus logo and icon assets.
|
||||
|
||||
### Build invocation
|
||||
|
||||
```bash
|
||||
$ grep -rln "Safe{Wallet}\|safe.global\|Safe Wallet" \
|
||||
--include='*.tsx' --include='*.ts' apps packages | wc -l
|
||||
1759
|
||||
# From repo root.
|
||||
yarn workspace @safe-global/web build:lux # Lux Safe
|
||||
yarn workspace @safe-global/web build:hanzo # Hanzo Vault
|
||||
yarn workspace @safe-global/web build:safe # Upstream Safe defaults (sanity check)
|
||||
```
|
||||
|
||||
≈1,759 files carry one or more brand references. Each falls into a
|
||||
small set of patterns:
|
||||
Under the hood each `build:<slug>` runs
|
||||
`scripts/select-brand.sh <slug>` then `yarn build`. The selector:
|
||||
|
||||
| Pattern | Replacement |
|
||||
|--------------------------------------|--------------------------------------|
|
||||
| `'Safe{Wallet}'` | `brand.name` |
|
||||
| `'Safe'` (subject noun) | `brand.shortName` |
|
||||
| `https://safe.global/...` | `${brand.helpUrl}/...` |
|
||||
| `support@safe.global` | `brand.email` |
|
||||
| `/images/safe-logo.svg` | `brand.logoUrl` |
|
||||
| `app.safe.global` | `brand.domain` |
|
||||
| Tweet IDs / Discord links | `brand.twitterHandle`, `discordUrl` |
|
||||
| `<meta name="apple-mobile-web-app-…` | per-brand `app.json` (mobile only) |
|
||||
1. Copies `apps/web/.env.<slug>` → `apps/web/.env.local`
|
||||
2. Copies `apps/web/public/brand/<slug>/` → `apps/web/public/brand/active/`
|
||||
3. Copies `apps/web/public/brand/<slug>/manifest.json` →
|
||||
`apps/web/public/safe.webmanifest`
|
||||
|
||||
## Implementation phases
|
||||
`apps/web/.env.local` and `apps/web/public/brand/active/` should be in
|
||||
`.gitignore` (or treated as build outputs) so the local-dev brand selection
|
||||
never leaks into commits.
|
||||
|
||||
1. **packages/brand**: create the package + types + default Brand
|
||||
pointing at the upstream Safe values (so the unbranded build
|
||||
matches today exactly).
|
||||
2. **Mechanical replace pass 1** — the easy patterns (URLs, email,
|
||||
logo paths). ~700 files via a single codemod.
|
||||
3. **Mechanical replace pass 2** — text-in-JSX (`"Safe Wallet"` →
|
||||
`{brand.name}`). ~900 files via a JSX-aware codemod.
|
||||
4. **Manual pass 3** — anything the codemod can't safely rewrite
|
||||
(filenames, asset URLs that need per-brand assets, metadata in
|
||||
`apps/web/public/manifest.json`, native `Info.plist`/`AndroidManifest`).
|
||||
5. **Per-brand build configs** — `apps/web/.env.lux`, `apps/web/.env.hanzo`,
|
||||
plus matching `mobile/app.config.lux.ts` / `app.config.hanzo.ts`.
|
||||
6. **Asset pipelines** — brand-token-driven Tailwind theme,
|
||||
per-brand `public/brand/` directory, build step that copies the
|
||||
selected brand's assets in.
|
||||
### Adding a new brand
|
||||
|
||||
## What stays upstream
|
||||
1. Add `apps/web/.env.<slug>` with `NEXT_PUBLIC_BRAND_*` values.
|
||||
2. Add `apps/web/public/brand/<slug>/` with `manifest.json`, `logo.svg`,
|
||||
`favicon.ico`, and the Android Chrome icon sizes referenced by the
|
||||
manifest.
|
||||
3. Add a `build:<slug>` script to `apps/web/package.json`.
|
||||
4. (Mobile) wire `EXPO_PUBLIC_BRAND_*` into the EAS build profile in
|
||||
`apps/mobile/eas.json`.
|
||||
|
||||
- Smart contract addresses (those are Safe's protocol — every brand
|
||||
uses the same contracts on each chain).
|
||||
- Public-API endpoint paths (the Safe Transaction Service URL
|
||||
schema; we just point the SDK at a per-network host).
|
||||
- The `safe-` prefix in npm package names of upstream deps. We don't
|
||||
rename `@safe-global/protocol-kit` etc.; that's the SDK, not the
|
||||
brand.
|
||||
That's the entire surface area. No source file changes needed.
|
||||
|
||||
## Tracking
|
||||
## Status
|
||||
|
||||
This file is the single source of truth for the white-label scope.
|
||||
Update it as phases land.
|
||||
| Phase | Status | Commit prefix |
|
||||
| ----- | ------ | ----------------------------------------------------------------------------- |
|
||||
| 0 | landed | `feat(brand): create @safe-global/brand workspace` |
|
||||
| 1 | landed | `refactor(brand): pass 1 — URLs, emails, and asset paths route through brand` |
|
||||
| 2 | landed | `refactor(brand): pass 2 — product name in JSX, copy, and SDK metadata` |
|
||||
| 3 | landed | `refactor(brand): pass 3 — manifests and metadata` |
|
||||
| 4 | landed | `feat(brand): per-brand build configs and asset pipeline` |
|
||||
|
||||
After all phases land, `grep -RIn 'safe\.global\|support@safe\.global\|Safe{Wallet}' apps/ packages/`
|
||||
returns only protocol references (the CGW/tx-service hosts) and the brand
|
||||
package's own defaults — no brand strings in app source.
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"dev:full": "cross-env USE_RSPACK=0 ENABLE_PWA=1 ENABLE_EXPERIMENTAL_OPTIMIZATIONS=1 next dev",
|
||||
"start": "next dev",
|
||||
"build": "yarn fetch-chains && next build",
|
||||
"build:safe": "../../scripts/select-brand.sh safe && yarn build",
|
||||
"build:lux": "../../scripts/select-brand.sh lux && yarn build",
|
||||
"build:hanzo": "../../scripts/select-brand.sh hanzo && yarn build",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* codemod-brand-urls.mjs
|
||||
*
|
||||
* Pass 1 of the white-label sweep. Replace mechanical URL / email / domain /
|
||||
* logo-path literals in app + package source with reads off @safe-global/brand.
|
||||
*
|
||||
* Strategy: this is a *targeted* rewrite, not a blind substitution. We touch
|
||||
* only string contexts that look like ES expressions — `'literal'` or
|
||||
* `"literal"` with a non-`=` non-`}` character immediately before the
|
||||
* opening quote. That keeps us out of:
|
||||
*
|
||||
* - JSX attributes: href="https://..." (preceded by `=`)
|
||||
* - JSX text: >Safe{Wallet}< (not in scope here — Pass 2)
|
||||
* - Template-literal placeholders we already produced ourselves
|
||||
*
|
||||
* Skip:
|
||||
* - tests, stories, cypress, fixtures, mocks, AUTO_GENERATED, node_modules,
|
||||
* dist, build, coverage
|
||||
* - @safe-global/brand itself
|
||||
* - safe-client.*, safe-transaction-*, safe-transaction-assets.*,
|
||||
* api.safe.global, *.5afe.dev (protocol/CDN, not brand)
|
||||
*
|
||||
* Import insertion: we add `import { brand } from '@safe-global/brand'` on
|
||||
* its own line *after* the last existing top-of-file import statement (we
|
||||
* find it by walking the AST-light way — find the last `from '…'` line
|
||||
* before any non-import token in column 0).
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { execSync } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
|
||||
const repoRoot = path.resolve(process.argv[1], '../..')
|
||||
process.chdir(repoRoot)
|
||||
|
||||
const excludePatterns = [
|
||||
/\/node_modules\//,
|
||||
/\/AUTO_GENERATED\//,
|
||||
/\/__tests__\//,
|
||||
/\/__mocks__\//,
|
||||
/\/cypress\//,
|
||||
/\/fixtures\//,
|
||||
/\.test\.tsx?$/,
|
||||
/\.stories\.tsx?$/,
|
||||
/\.cy\.tsx?$/,
|
||||
/\.cy\.js$/,
|
||||
/\.spec\.tsx?$/,
|
||||
/^packages\/brand\//,
|
||||
/\/dist\//,
|
||||
/\/build\//,
|
||||
/\/coverage\//,
|
||||
/^scripts\//,
|
||||
]
|
||||
|
||||
function listFiles() {
|
||||
const out = execSync(
|
||||
"git ls-files 'apps/*.ts' 'apps/*.tsx' 'apps/**/*.ts' 'apps/**/*.tsx' 'packages/*.ts' 'packages/*.tsx' 'packages/**/*.ts' 'packages/**/*.tsx'",
|
||||
{ encoding: 'utf8' },
|
||||
)
|
||||
return out
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.filter((p) => !excludePatterns.some((re) => re.test(p)))
|
||||
}
|
||||
|
||||
// Map of literal -> brand-property replacement. Each value is the ES
|
||||
// expression we want to substitute in.
|
||||
//
|
||||
// `safeUrl` flag means: when the literal appears in a JSX attribute string
|
||||
// position (preceded by `=`), wrap with braces — `="x"` becomes `={x}`.
|
||||
// Currently we DON'T touch JSX attribute strings at all (they're handled by
|
||||
// Pass 2 which can rewrite them safely). We only touch ES-string positions.
|
||||
const literalReplacements = [
|
||||
// marketing safe.global with named paths
|
||||
{ lit: 'https://safe.global/terms', expr: 'brand.termsUrl' },
|
||||
{ lit: 'https://safe.global/privacy', expr: 'brand.privacyUrl' },
|
||||
{ lit: 'https://safe.global/imprint', expr: 'brand.imprintUrl' },
|
||||
{ lit: 'https://safe.global/cookie', expr: 'brand.cookieUrl' },
|
||||
{ lit: 'https://safe.global/licenses', expr: 'brand.licensesUrl' },
|
||||
// primary services
|
||||
{ lit: 'https://help.safe.global', expr: 'brand.helpUrl' },
|
||||
{ lit: 'https://status.safe.global', expr: 'brand.statusUrl' },
|
||||
{ lit: 'https://developer.safe.global', expr: 'brand.developerUrl' },
|
||||
{ lit: 'https://chat.safe.global', expr: 'brand.discordUrl' },
|
||||
{ lit: 'https://twitter.com/safe', expr: 'brand.twitterUrl' },
|
||||
{ lit: 'https://app.safe.global', expr: 'brand.appUrl' },
|
||||
{ lit: 'support@safe.global', expr: 'brand.email' },
|
||||
{ lit: 'app.safe.global', expr: 'brand.appHost' },
|
||||
{ lit: 'anon.safe.global', expr: 'brand.supportChatAliasDomain' },
|
||||
// logo assets
|
||||
{ lit: '/images/safe-logo-green.png', expr: 'brand.logoUrl' },
|
||||
{ lit: '/images/safe-logo.svg', expr: 'brand.logoUrl' },
|
||||
]
|
||||
|
||||
const importLine = "import { brand } from '@safe-global/brand'"
|
||||
|
||||
/**
|
||||
* Insert `importLine` after the last top-of-file import statement. The
|
||||
* file's import block is the contiguous run of lines starting with `import`
|
||||
* (potentially spanning multiple lines for `import {\n a,\n b,\n} from …`),
|
||||
* starting at top of file and continuing until we see a non-import top-
|
||||
* level statement.
|
||||
*/
|
||||
function ensureImport(content) {
|
||||
if (content.includes("from '@safe-global/brand'")) return content
|
||||
// Tokenize line by line. We keep accepting lines into the "import block"
|
||||
// while a JS-aware predicate says we're still in an import.
|
||||
const lines = content.split('\n')
|
||||
let lastImportEndIdx = -1
|
||||
let i = 0
|
||||
while (i < lines.length) {
|
||||
const line = lines[i]
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('import ') || trimmed === 'import {') {
|
||||
// Walk until we find the line that closes this import — either a
|
||||
// semicolon at the end, OR a line whose content is `from '…'`. For
|
||||
// robustness we just keep going until the line ends with a quote or
|
||||
// a semicolon.
|
||||
let j = i
|
||||
while (j < lines.length) {
|
||||
const t = lines[j].trim()
|
||||
if (t.endsWith("'") || t.endsWith('"') || t.endsWith(';')) {
|
||||
lastImportEndIdx = j
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
i = (lastImportEndIdx >= i ? lastImportEndIdx : i) + 1
|
||||
continue
|
||||
}
|
||||
if (trimmed === '' || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
|
||||
// Allow blank lines and comments inside the import block.
|
||||
i++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if (lastImportEndIdx >= 0) {
|
||||
lines.splice(lastImportEndIdx + 1, 0, importLine)
|
||||
} else {
|
||||
lines.unshift(importLine)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace `'literal'` and `"literal"` with `expr` whenever the quote is in
|
||||
* an ES-expression position (i.e. NOT preceded by `=`, which signals a JSX
|
||||
* attribute). Returns the new content and a hit count.
|
||||
*
|
||||
* We also rewrite `'literal/path/...'` and `"literal/path/..."` into a
|
||||
* template literal `\`${expr}/path/...\``.
|
||||
*/
|
||||
function rewriteLiterals(content) {
|
||||
let hits = 0
|
||||
|
||||
for (const { lit, expr } of literalReplacements) {
|
||||
// 1. Bare literal: '<lit>' or "<lit>"
|
||||
// Allowed prefix: any char that is NOT `=` (JSX attribute) or `}` (rare).
|
||||
// We accept `(`, `,`, `:`, ` `, `\n`, etc.
|
||||
const escLit = escapeRegExp(lit)
|
||||
const bareSingle = new RegExp(`(^|[^=])'${escLit}'`, 'g')
|
||||
const bareDouble = new RegExp(`(^|[^=])"${escLit}"`, 'g')
|
||||
content = content.replace(bareSingle, (_m, pre) => {
|
||||
hits++
|
||||
return `${pre}${expr}`
|
||||
})
|
||||
content = content.replace(bareDouble, (_m, pre) => {
|
||||
hits++
|
||||
return `${pre}${expr}`
|
||||
})
|
||||
|
||||
// 2. Literal-with-path: '<lit>/foo/bar' → `${expr}/foo/bar`
|
||||
// Same prefix guard.
|
||||
const pathSingle = new RegExp(`(^|[^=])'${escLit}(/[^']+)'`, 'g')
|
||||
const pathDouble = new RegExp(`(^|[^=])"${escLit}(/[^"]+)"`, 'g')
|
||||
content = content.replace(pathSingle, (_m, pre, tail) => {
|
||||
hits++
|
||||
return `${pre}\`\${${expr}}${tail}\``
|
||||
})
|
||||
content = content.replace(pathDouble, (_m, pre, tail) => {
|
||||
hits++
|
||||
return `${pre}\`\${${expr}}${tail}\``
|
||||
})
|
||||
}
|
||||
return { content, hits }
|
||||
}
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
let touched = 0
|
||||
let totalReplacements = 0
|
||||
const files = listFiles()
|
||||
for (const file of files) {
|
||||
const before = readFileSync(file, 'utf8')
|
||||
const { content: rewritten, hits } = rewriteLiterals(before)
|
||||
if (hits === 0) continue
|
||||
const withImport = ensureImport(rewritten)
|
||||
if (withImport === before) continue
|
||||
writeFileSync(file, withImport)
|
||||
touched++
|
||||
totalReplacements += hits
|
||||
console.log(` ${file} (${hits})`)
|
||||
}
|
||||
|
||||
console.log(`\nfiles touched: ${touched}`)
|
||||
console.log(`replacements: ${totalReplacements}`)
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# select-brand.sh — switch the active brand for a local build.
|
||||
#
|
||||
# Usage: scripts/select-brand.sh <slug>
|
||||
# Slugs: safe | lux | hanzo
|
||||
#
|
||||
# What it does:
|
||||
# 1) Copies apps/web/.env.<slug> -> apps/web/.env.local (overridden each run)
|
||||
# 2) Mirrors apps/web/public/brand/<slug>/ -> apps/web/public/brand/active/
|
||||
#
|
||||
# After this runs, `yarn workspace @safe-global/web build` produces the branded build.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
slug="${1:-}"
|
||||
|
||||
if [[ -z "$slug" ]]; then
|
||||
echo "usage: scripts/select-brand.sh <slug>"
|
||||
echo "available brands:"
|
||||
for d in apps/web/public/brand/*/; do
|
||||
name="$(basename "$d")"
|
||||
[[ "$name" == "active" ]] && continue
|
||||
echo " - $name"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
env_file="$repo_root/apps/web/.env.$slug"
|
||||
brand_dir="$repo_root/apps/web/public/brand/$slug"
|
||||
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "error: $env_file not found"
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$brand_dir" ]]; then
|
||||
echo "error: $brand_dir not found"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cp "$env_file" "$repo_root/apps/web/.env.local"
|
||||
|
||||
rm -rf "$repo_root/apps/web/public/brand/active"
|
||||
mkdir -p "$repo_root/apps/web/public/brand/active"
|
||||
cp -R "$brand_dir/." "$repo_root/apps/web/public/brand/active/"
|
||||
|
||||
# Per-brand manifest replaces the default safe.webmanifest at build time.
|
||||
if [[ -f "$brand_dir/manifest.json" ]]; then
|
||||
cp "$brand_dir/manifest.json" "$repo_root/apps/web/public/safe.webmanifest"
|
||||
fi
|
||||
|
||||
echo "brand '$slug' selected"
|
||||
echo " env -> apps/web/.env.local"
|
||||
echo " assets -> apps/web/public/brand/active/"
|
||||
echo " manifest -> apps/web/public/safe.webmanifest"
|
||||
Reference in New Issue
Block a user