rebrand-strings.ts211 lines · main
| 1 | #!/usr/bin/env tsx |
| 2 | /** |
| 3 | * scripts/rebrand-strings.ts |
| 4 | * |
| 5 | * Phase 3 of BACKEND_FORK_BRIEF.md — mechanical case-sensitive rebrand sweep |
| 6 | * across the vendored Studio fork. Run before the visual-restyle phases. |
| 7 | * |
| 8 | * Usage: |
| 9 | * pnpm tsx scripts/rebrand-strings.ts # default target: apps/studio |
| 10 | * pnpm tsx scripts/rebrand-strings.ts apps/studio # explicit target |
| 11 | * pnpm tsx scripts/rebrand-strings.ts --dry-run # report counts, no writes |
| 12 | * |
| 13 | * Replacements (case-sensitive): |
| 14 | * Supabase -> Briven |
| 15 | * supabase -> briven |
| 16 | * SUPABASE -> BRIVEN |
| 17 | * SUPA_ -> BRVN_ |
| 18 | * supa_ -> brvn_ |
| 19 | * |
| 20 | * Protected tokens (NOT rewritten — would break runtime): |
| 21 | * - @supabase/... (npm scope; published packages we cannot rename) |
| 22 | * - @supabase-labs/... (same) |
| 23 | * - supabase.com / .io / .co URLs and subdomains (Phase 4 handles via CSP/link audit) |
| 24 | * - .supabase. fragments inside hostnames (e.g. db.fqfdjxabc.supabase.co) |
| 25 | * |
| 26 | * Excluded paths: |
| 27 | * - node_modules/, .next/, .turbo/, dist/, build/, coverage/ |
| 28 | * - public/ (Phase 4: brand-asset rebrand) |
| 29 | * - lock files (pnpm-lock.yaml, package-lock.json, yarn.lock) |
| 30 | * - binary files (by extension) |
| 31 | */ |
| 32 | |
| 33 | import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; |
| 34 | import { join, extname, relative } from 'node:path'; |
| 35 | |
| 36 | const args = process.argv.slice(2); |
| 37 | const dryRun = args.includes('--dry-run'); |
| 38 | const targets = args.filter((a) => !a.startsWith('--')); |
| 39 | const rootTargets = targets.length > 0 ? targets : ['apps/studio']; |
| 40 | |
| 41 | const EXCLUDED_DIRS = new Set([ |
| 42 | 'node_modules', |
| 43 | '.next', |
| 44 | '.turbo', |
| 45 | '.tsup', |
| 46 | 'dist', |
| 47 | 'build', |
| 48 | 'coverage', |
| 49 | '.git', |
| 50 | 'public', |
| 51 | ]); |
| 52 | |
| 53 | const BINARY_EXTENSIONS = new Set([ |
| 54 | '.png', |
| 55 | '.jpg', |
| 56 | '.jpeg', |
| 57 | '.gif', |
| 58 | '.svg', |
| 59 | '.ico', |
| 60 | '.webp', |
| 61 | '.avif', |
| 62 | '.woff', |
| 63 | '.woff2', |
| 64 | '.ttf', |
| 65 | '.otf', |
| 66 | '.eot', |
| 67 | '.mp4', |
| 68 | '.webm', |
| 69 | '.mp3', |
| 70 | '.wav', |
| 71 | '.ogg', |
| 72 | '.pdf', |
| 73 | '.zip', |
| 74 | '.gz', |
| 75 | '.tar', |
| 76 | '.7z', |
| 77 | '.wasm', |
| 78 | '.node', |
| 79 | '.so', |
| 80 | '.dylib', |
| 81 | '.dll', |
| 82 | '.lock', |
| 83 | '.snap', |
| 84 | ]); |
| 85 | |
| 86 | const SKIP_FILENAMES = new Set(['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lockb']); |
| 87 | |
| 88 | const PROTECT_PATTERNS: RegExp[] = [ |
| 89 | /@supabase-labs\/[a-z0-9-]+/g, |
| 90 | /@supabase\/[a-z0-9-]+/g, |
| 91 | /\b[a-z0-9-]+\.supabase\.(?:co|com|io)\b[^\s'"\)<>]*/g, |
| 92 | /\bsupabase\.com\b[^\s'"\)<>]*/g, |
| 93 | /\bsupabase\.io\b[^\s'"\)<>]*/g, |
| 94 | /\bsupabase\.co\b[^\s'"\)<>]*/g, |
| 95 | /\bgithub\.com\/supabase[a-z0-9_-]*\/[^\s'"\)<>]+/g, |
| 96 | /\bgithub\.com\/orgs\/supabase[^\s'"\)<>]*/g, |
| 97 | /\bsupabase_(?:admin|auth_admin|storage_admin|functions_admin|realtime_admin|replication_admin|read_only_user)\b/g, |
| 98 | /\bsupabase_(?:functions|migrations)\b/g, |
| 99 | /\bclient_connections_supabase_[a-z_]+\b/g, |
| 100 | ]; |
| 101 | |
| 102 | const REPLACEMENTS: Array<[RegExp, string]> = [ |
| 103 | [/Supabase/g, 'Briven'], |
| 104 | [/SUPABASE/g, 'BRIVEN'], |
| 105 | [/SUPA_/g, 'BRVN_'], |
| 106 | [/supa_/g, 'brvn_'], |
| 107 | [/supabase/g, 'briven'], |
| 108 | ]; |
| 109 | |
| 110 | interface FileResult { |
| 111 | path: string; |
| 112 | matches: number; |
| 113 | protected: number; |
| 114 | } |
| 115 | |
| 116 | function walk(dir: string, out: string[]): void { |
| 117 | let entries; |
| 118 | try { |
| 119 | entries = readdirSync(dir, { withFileTypes: true }); |
| 120 | } catch { |
| 121 | return; |
| 122 | } |
| 123 | for (const e of entries) { |
| 124 | if (EXCLUDED_DIRS.has(e.name)) continue; |
| 125 | const p = join(dir, e.name); |
| 126 | if (e.isDirectory()) { |
| 127 | walk(p, out); |
| 128 | } else if (e.isFile()) { |
| 129 | if (SKIP_FILENAMES.has(e.name)) continue; |
| 130 | if (BINARY_EXTENSIONS.has(extname(e.name).toLowerCase())) continue; |
| 131 | out.push(p); |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | function rebrand(content: string): { out: string; matches: number; protectedCount: number } { |
| 137 | const placeholders: string[] = []; |
| 138 | let working = content; |
| 139 | let protectedCount = 0; |
| 140 | |
| 141 | for (const pat of PROTECT_PATTERNS) { |
| 142 | working = working.replace(pat, (m) => { |
| 143 | const idx = placeholders.length; |
| 144 | placeholders.push(m); |
| 145 | protectedCount++; |
| 146 | return `PROTECT${idx}`; |
| 147 | }); |
| 148 | } |
| 149 | |
| 150 | let matches = 0; |
| 151 | for (const [pat, repl] of REPLACEMENTS) { |
| 152 | working = working.replace(pat, () => { |
| 153 | matches++; |
| 154 | return repl; |
| 155 | }); |
| 156 | } |
| 157 | |
| 158 | working = working.replace(/PROTECT(\d+)/g, (_m, idx) => placeholders[Number(idx)]); |
| 159 | |
| 160 | return { out: working, matches, protectedCount }; |
| 161 | } |
| 162 | |
| 163 | function main(): void { |
| 164 | const repoRoot = process.cwd(); |
| 165 | const files: string[] = []; |
| 166 | for (const t of rootTargets) { |
| 167 | walk(t, files); |
| 168 | } |
| 169 | |
| 170 | const results: FileResult[] = []; |
| 171 | let totalMatches = 0; |
| 172 | let totalProtected = 0; |
| 173 | let filesChanged = 0; |
| 174 | |
| 175 | for (const f of files) { |
| 176 | let raw: string; |
| 177 | try { |
| 178 | raw = readFileSync(f, 'utf8'); |
| 179 | } catch { |
| 180 | continue; |
| 181 | } |
| 182 | const { out, matches, protectedCount } = rebrand(raw); |
| 183 | totalProtected += protectedCount; |
| 184 | if (matches === 0) continue; |
| 185 | results.push({ path: relative(repoRoot, f), matches, protected: protectedCount }); |
| 186 | totalMatches += matches; |
| 187 | filesChanged++; |
| 188 | if (!dryRun) { |
| 189 | writeFileSync(f, out, 'utf8'); |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | results.sort((a, b) => b.matches - a.matches); |
| 194 | const topN = 25; |
| 195 | for (const r of results.slice(0, topN)) { |
| 196 | process.stdout.write(`${String(r.matches).padStart(5)} ${r.path}\n`); |
| 197 | } |
| 198 | if (results.length > topN) { |
| 199 | process.stdout.write(` ... ${results.length - topN} more files\n`); |
| 200 | } |
| 201 | process.stdout.write( |
| 202 | `\n${dryRun ? '[dry-run] ' : ''}` + |
| 203 | `targets=${rootTargets.join(',')} ` + |
| 204 | `scanned=${files.length} ` + |
| 205 | `changed=${filesChanged} ` + |
| 206 | `replacements=${totalMatches} ` + |
| 207 | `protected=${totalProtected}\n`, |
| 208 | ); |
| 209 | } |
| 210 | |
| 211 | main(); |