Files
Xu Liang 708a45ba16
Desktop Client Build / Resolve Build Metadata (push) Successful in 19s
Desktop Client Build / Build Desktop Client (push) Successful in 21m39s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
fix(desktop): skip obfuscation for electron main to preserve page.evaluate
Playwright serializes page.evaluate callbacks via Function.prototype.toString and runs them in the target page context. Obfuscating the main bundle injected string-array helper references that the browser page could not resolve, surfacing as `ReferenceError: wO is not defined` for bilibili and dongchedi publishes. Mirrors the existing Vite `minify: false` guard for main/preload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:46:37 +08:00

206 lines
6.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-require-imports */
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const JavaScriptObfuscator = require('javascript-obfuscator')
const remapping = require('@ampproject/remapping')
const projectRoot = path.resolve(__dirname, '..')
const outDir = path.join(projectRoot, 'out')
const baseOptions = {
compact: true,
controlFlowFlattening: false,
deadCodeInjection: false,
debugProtection: false,
disableConsoleOutput: false,
identifierNamesGenerator: 'mangled-shuffled',
log: false,
numbersToExpressions: true,
renameGlobals: false,
rotateStringArray: true,
selfDefending: false,
shuffleStringArray: true,
simplify: true,
splitStrings: true,
splitStringsChunkLength: 10,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 0.5,
stringArrayEncoding: ['base64'],
stringArrayIndexShift: true,
stringArrayRotate: true,
stringArrayShuffle: true,
stringArrayThreshold: 0.75,
transformObjectKeys: false,
unicodeEscapeSequence: false,
sourceMap: true,
sourceMapMode: 'separate',
sourceMapSourcesMode: 'sources-content',
}
const targets = [
// Keep the Electron main process unobfuscated. It owns the Playwright
// adapters, and Playwright serializes `page.evaluate` callbacks with
// Function.prototype.toString(). Obfuscator string-array helpers live in the
// main bundle, not in the target web page, so packaged media publishers such
// as bilibili and dongchedi can fail with "ReferenceError: <helper> is not
// defined" inside the browser context.
{
label: 'renderer',
dir: path.join(outDir, 'renderer', 'assets'),
match: (name) => name.endsWith('.js'),
options: { ...baseOptions, target: 'browser' },
},
]
function walk(dir) {
const result = []
if (!fs.existsSync(dir)) return result
const stack = [dir]
while (stack.length > 0) {
const current = stack.pop()
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const full = path.join(current, entry.name)
if (entry.isDirectory()) {
stack.push(full)
} else if (entry.isFile()) {
result.push(full)
}
}
}
return result
}
function formatBytes(n) {
if (n < 1024) return `${n}B`
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`
return `${(n / 1024 / 1024).toFixed(2)}MB`
}
function obfuscateFile(file, options) {
const original = fs.readFileSync(file, 'utf8')
const beforeBytes = Buffer.byteLength(original, 'utf8')
const inputFileName = path.basename(file)
const sourceMapFileName = `${inputFileName}.map`
const viteMapPath = `${file}.map`
// vite 以 sourcemap: 'hidden' 输出时会把 .map 写在文件旁边,但不在 bundle 末尾加注释。
// 在混淆前把这份 map 读出来,混淆后用 remapping chain 出 obfuscated → ts。
let viteMap = null
if (fs.existsSync(viteMapPath)) {
try {
viteMap = JSON.parse(fs.readFileSync(viteMapPath, 'utf8'))
} catch {
viteMap = null
}
}
const result = JavaScriptObfuscator.obfuscate(original, {
...options,
inputFileName,
sourceMapFileName,
})
let code = result.getObfuscatedCode()
// javascript-obfuscator 对 sourceMapFileName 做扩展名规整(.cjs → .js),
// 会让注释指向 bootstrap.js.map 而真实文件是 bootstrap.cjs.map — 重写注释修正。
code = code.replace(
/\/\/# sourceMappingURL=[^\n]*$/,
`//# sourceMappingURL=${sourceMapFileName}`,
)
fs.writeFileSync(file, code, 'utf8')
const afterBytes = Buffer.byteLength(code, 'utf8')
let mapBytes = 0
let chained = false
if (options.sourceMap) {
let mapJson = result.getSourceMap()
if (mapJson) {
if (viteMap) {
try {
const obfMap = JSON.parse(mapJson)
// remapping 会递归地为 sources 数组里的每一项调 loader。
// 我们只想为 obfMap 自己的 sources 返回 viteMapchain 一层),
// 对 viteMap 内部的 ts 路径返回 null,否则 loader 会无限递归 → stack overflow。
const obfSources = new Set(obfMap.sources || [])
const chainedMap = remapping(obfMap, (file) =>
obfSources.has(file) ? viteMap : null,
)
mapJson = chainedMap.toString()
chained = true
} catch (err) {
console.warn(
` ! ${path.relative(projectRoot, file)} chain failed, keeping obfuscator-only map: ${err.message}`,
)
}
}
fs.writeFileSync(viteMapPath, mapJson, 'utf8')
mapBytes = Buffer.byteLength(mapJson, 'utf8')
}
}
return { beforeBytes, afterBytes, mapBytes, chained }
}
function main() {
if (!fs.existsSync(outDir)) {
console.error(`[obfuscate] missing build output: ${outDir} — run \`pnpm build\` first.`)
process.exit(1)
}
let totalFiles = 0
let totalBefore = 0
let totalAfter = 0
let totalMap = 0
let totalChained = 0
let failed = 0
for (const target of targets) {
if (!fs.existsSync(target.dir)) {
console.warn(`[obfuscate] skip ${target.label}: directory not found (${target.dir})`)
continue
}
const files = walk(target.dir).filter((f) => target.match(path.basename(f)))
if (files.length === 0) {
console.warn(`[obfuscate] skip ${target.label}: no matching files in ${target.dir}`)
continue
}
console.log(`[obfuscate] ${target.label}: ${files.length} file(s)`)
for (const file of files) {
const rel = path.relative(projectRoot, file)
try {
const { beforeBytes, afterBytes, mapBytes, chained } = obfuscateFile(file, target.options)
totalFiles += 1
totalBefore += beforeBytes
totalAfter += afterBytes
totalMap += mapBytes
if (chained) totalChained += 1
const ratio = ((afterBytes / beforeBytes) * 100).toFixed(0)
const mapNote = mapBytes > 0 ? ` +map ${formatBytes(mapBytes)}${chained ? ' (chained → ts)' : ''}` : ''
console.log(
`${rel} ${formatBytes(beforeBytes)}${formatBytes(afterBytes)} (${ratio}%)${mapNote}`,
)
} catch (err) {
failed += 1
console.error(`${rel} ${err && err.message ? err.message : err}`)
}
}
}
const mapSummary = totalMap > 0
? `, sourcemaps ${formatBytes(totalMap)} (${totalChained}/${totalFiles} chained → ts, excluded from package)`
: ''
console.log(
`[obfuscate] done: ${totalFiles} file(s), ${formatBytes(totalBefore)}${formatBytes(totalAfter)}${mapSummary}` +
(failed > 0 ? `, ${failed} failed` : ''),
)
if (failed > 0) {
process.exit(1)
}
}
main()