mateclaw/mateclaw-desktop/scripts/branding.cjs
Joe0720 e79fb00fec
feat(desktop): support remote lite build mode without bundled JRE/JAR (#417)
* feat(desktop): support remote lite build mode without bundled JRE/JAR

Add a dual packaging mode system controlled by the BUILD_MODE env var:

- **local** (default): Full build bundling JRE + Spring Boot JAR, identical
  to the previous behavior.  Supports both embedded local backend and
  remote server connection.

- **remote** (lite): Omits the ~530 MB JRE/JAR resources, producing an
  installer that is ~81% smaller (97 MB vs 523 MB on macOS arm64).  The
  app only supports connecting to a remote server; the "local" option is
  hidden from the splash connection chooser.

Changes:
- Replace static electron-builder.json with dynamic electron-builder.cjs
  that conditionally includes extraResources based on BUILD_MODE
- Add build mode detection at runtime (checks JAR existence) with graceful
  fallback to remote-only mode
- Add IPC handler app:get-build-mode and expose via preload
- Hide "本地运行" option in splash when running a remote build
- Ignore stale 'local' saved config in remote builds
- Add package scripts: package:mac:local, package:mac:remote, etc.
- Add missing build scripts: build.sh, download-jre.sh, build-all-platforms.sh
- Add no-op afterPack hook (trim-playwright-driver.cjs) placeholder
- Add cross-env devDependency for cross-platform BUILD_MODE support

* feat(desktop): add white-label branding system for build-time rebranding

Add a Vite plugin (scripts/branding.cjs) that replaces hardcoded "MateClaw"
strings at build time, enabling white-label/OEM rebranding without modifying
any source code.

Configuration:
- Edit branding.config.json (name, tagline, team, copyright, appId, githubUrl)
- Or set BRAND_* env vars (BRAND_NAME, BRAND_TAGLINE, BRAND_TEAM, etc.)

Usage:
  # Default build (MateClaw brand)
  npm run package:mac

  # Custom brand via env vars
  BRAND_NAME=MyAI BRAND_TAGLINE="Smart AI Helper" npm run package:mac:remote

  # Or edit branding.config.json and build normally
  npm run package:mac:remote

Replacements applied at build time:
- Brand name (window title, About dialog, error messages, console logs)
- Tagline, team name, copyright line
- GitHub repo/issues URLs
- Logo file path
- electron-builder config (productName, appId, artifactName, dmg title, publish repo)

The branding plugin runs in Vite's transform hook, covering the renderer
(App.vue, index.html), electron main process, and preload script.

Server-coupled strings (H2 database name, Spring Boot property names) are
intentionally NOT replaced to avoid breaking backend compatibility.

---------

Co-authored-by: qiaozhipeng <qiaozhipeng@daojia-inc.com>
2026-06-26 09:29:24 +08:00

154 lines
4.8 KiB
JavaScript

/**
* scripts/branding.cjs — Vite plugin for build-time white-label branding.
*
* Reads brand settings from branding.config.json (or BRAND_* env overrides)
* and replaces hardcoded "MateClaw" strings in all built files — source code
* stays untouched.
*
* Supported env overrides:
* BRAND_NAME, BRAND_TAGLINE, BRAND_TEAM, BRAND_COPYRIGHT,
* BRAND_APP_ID, BRAND_GITHUB_URL, BRAND_LOGO_FILE
*/
'use strict'
const fs = require('fs')
const path = require('path')
function loadBrandConfig(rootDir) {
const configPath = path.join(rootDir, 'branding.config.json')
let config = {}
if (fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
}
// Env vars override the config file.
const env = process.env
return {
name: env.BRAND_NAME || config.name || 'MateClaw',
tagline: env.BRAND_TAGLINE || config.tagline || 'AI Personal Assistant',
team: env.BRAND_TEAM || config.team || 'MateClaw Team',
copyright: env.BRAND_COPYRIGHT || config.copyright || 'Copyright © 2026 MateClaw Team',
appId: env.BRAND_APP_ID || config.appId || 'vip.mate.mateclaw',
githubUrl: env.BRAND_GITHUB_URL || config.githubUrl || 'https://github.com/matevip/mateclaw',
logoFile: env.BRAND_LOGO_FILE || config.logoFile || 'mateclaw_logo_s.png',
}
}
/**
* Build the string-replacement table.
*
* Order matters: longer/more-specific patterns are replaced first to avoid
* partial matches (e.g. "MateClaw Team" before "MateClaw").
*/
function buildReplacements(brand) {
const replacements = []
// 1. Copyright line (most specific)
replacements.push([
'Copyright © 2026 MateClaw Team',
brand.copyright,
])
// 2. Team name
replacements.push(['MateClaw Team', brand.team])
// 3. GitHub URLs
replacements.push([
'https://github.com/matevip/mateclaw/issues',
brand.githubUrl + '/issues',
])
replacements.push([
'https://github.com/matevip/mateclaw',
brand.githubUrl,
])
// 4. Logo file path
replacements.push([
'mateclaw_logo_s.png',
brand.logoFile,
])
// 5. Tagline
replacements.push([
'AI Personal Assistant',
brand.tagline,
])
// 6. Split-span brand name in App.vue template:
// <span class="mate">Mate</span><span class="claw">Claw</span>
// Replace the inner text so styling classes are preserved but the text
// changes. We split the brand name: first half gets "mate" class, second
// half gets "claw" class. If it's a single word, it all goes in "mate".
var half = Math.ceil(brand.name.length / 2)
var firstPart = brand.name.slice(0, half)
var secondPart = brand.name.slice(half)
replacements.push([
'>Mate</span><span class="claw">Claw<',
'>' + firstPart + '</span><span class="claw">' + secondPart + '<',
])
// 7. Brand name (catch-all, must come last)
// Only replace the exact word "MateClaw", not "mateclaw" (lowercase,
// which is used in H2 database paths and Spring Boot properties that
// are coupled with the server and must NOT change).
replacements.push(['MateClaw', brand.name])
return replacements
}
function applyReplacements(code, replacements) {
var result = code
for (var i = 0; i < replacements.length; i++) {
var from = replacements[i][0]
var to = replacements[i][1]
// Use split/join for reliable literal string replacement (no regex
// escaping issues).
result = result.split(from).join(to)
}
return result
}
/**
* Vite plugin entry point.
*
* Usage in vite.config.ts:
* import { brandingPlugin } from './scripts/branding.cjs'
* plugins: [brandingPlugin()]
*/
function brandingPlugin(options) {
options = options || {}
var rootDir = options.rootDir || process.cwd()
var brand = loadBrandConfig(rootDir)
var replacements = buildReplacements(brand)
var isDefault =
brand.name === 'MateClaw' &&
brand.tagline === 'AI Personal Assistant' &&
brand.team === 'MateClaw Team'
if (!isDefault) {
console.log('[branding] White-label build: "' + brand.name + '" (tagline: "' + brand.tagline + '")')
}
return {
name: 'mateclaw-branding',
enforce: 'pre',
// Transform JS/TS/Vue source before compilation
transform: function (code, id) {
if (id.indexOf('node_modules') !== -1) return null
// Only process source files that might contain brand strings.
if (!/\.(ts|js|vue|html|css|cjs|mjs)$/.test(id)) return null
var result = applyReplacements(code, replacements)
return result !== code ? { code: result, map: null } : null
},
// Transform index.html
transformIndexHtml: function (html) {
return applyReplacements(html, replacements)
},
}
}
module.exports = { brandingPlugin: brandingPlugin, loadBrandConfig: loadBrandConfig, buildReplacements: buildReplacements }