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
33import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
34import { join, extname, relative } from 'node:path';
35
36const args = process.argv.slice(2);
37const dryRun = args.includes('--dry-run');
38const targets = args.filter((a) => !a.startsWith('--'));
39const rootTargets = targets.length > 0 ? targets : ['apps/studio'];
40
41const 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
53const 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
86const SKIP_FILENAMES = new Set(['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lockb']);
87
88const 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
102const 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
110interface FileResult {
111 path: string;
112 matches: number;
113 protected: number;
114}
115
116function 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
136function 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
163function 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
211main();