feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readdirSync, statSync } from 'node:fs'
|
||||
import { join, relative } from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
const root = process.cwd()
|
||||
const distDir = join(root, 'dist')
|
||||
|
||||
const budgets = {
|
||||
maxDistBytes: Number(process.env.MAX_DIST_BYTES || 800 * 1024),
|
||||
maxJsChunkBytes: Number(process.env.MAX_JS_CHUNK_BYTES || 300 * 1024),
|
||||
maxPackSizeBytes: Number(process.env.MAX_PACK_TGZ_BYTES || 250 * 1024),
|
||||
maxPackUnpackedBytes: Number(process.env.MAX_PACK_UNPACKED_BYTES || 740 * 1024),
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024)
|
||||
return `${bytes} B`
|
||||
if (bytes < 1024 * 1024)
|
||||
return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
|
||||
}
|
||||
|
||||
function collectFiles(dir, files = []) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const fullPath = join(dir, name)
|
||||
const stat = statSync(fullPath)
|
||||
if (stat.isDirectory()) {
|
||||
collectFiles(fullPath, files)
|
||||
}
|
||||
else {
|
||||
files.push({
|
||||
fullPath,
|
||||
relPath: relative(root, fullPath),
|
||||
size: stat.size,
|
||||
})
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
const allFiles = collectFiles(distDir)
|
||||
const distBytes = allFiles.reduce((sum, f) => sum + f.size, 0)
|
||||
const jsFiles = allFiles.filter(f => f.relPath.endsWith('.js'))
|
||||
const largestJs = jsFiles.sort((a, b) => b.size - a.size)[0]
|
||||
|
||||
const packJson = execFileSync('npm', ['pack', '--dry-run', '--json'], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
const pack = JSON.parse(packJson)?.[0]
|
||||
|
||||
if (!pack)
|
||||
throw new Error('Failed to read npm pack --dry-run --json output.')
|
||||
|
||||
const failures = []
|
||||
if (distBytes > budgets.maxDistBytes) {
|
||||
failures.push(`dist total is ${formatBytes(distBytes)}, exceeds budget ${formatBytes(budgets.maxDistBytes)}`)
|
||||
}
|
||||
if (largestJs && largestJs.size > budgets.maxJsChunkBytes) {
|
||||
failures.push(`${largestJs.relPath} is ${formatBytes(largestJs.size)}, exceeds JS chunk budget ${formatBytes(budgets.maxJsChunkBytes)}`)
|
||||
}
|
||||
if (pack.size > budgets.maxPackSizeBytes) {
|
||||
failures.push(`npm tarball is ${formatBytes(pack.size)}, exceeds budget ${formatBytes(budgets.maxPackSizeBytes)}`)
|
||||
}
|
||||
if (pack.unpackedSize > budgets.maxPackUnpackedBytes) {
|
||||
failures.push(`npm unpacked size is ${formatBytes(pack.unpackedSize)}, exceeds budget ${formatBytes(budgets.maxPackUnpackedBytes)}`)
|
||||
}
|
||||
|
||||
console.log(`[size-check] dist total: ${formatBytes(distBytes)}`)
|
||||
if (largestJs)
|
||||
console.log(`[size-check] largest JS: ${largestJs.relPath} (${formatBytes(largestJs.size)})`)
|
||||
console.log(`[size-check] pack tarball: ${formatBytes(pack.size)}`)
|
||||
console.log(`[size-check] pack unpacked: ${formatBytes(pack.unpackedSize)}`)
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('[size-check] Budget check failed:')
|
||||
for (const line of failures)
|
||||
console.error(`- ${line}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('[size-check] Budget check passed.')
|
||||
Reference in New Issue
Block a user