Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ jobs:
- name: Build application
run: bun run build

- name: Check bundle sizes
run: ./scripts/check_bundle_size.sh

- name: Package for Linux
run: make dist-linux

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@ __pycache__

tmpfork
.cmux-agent-cli
*.local.md
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@
"output": "release"
},
"files": [
"dist/**/*"
"dist/**/*",
"!dist/**/*.map"
],
"asarUnpack": [
"dist/**/*.wasm",
"dist/**/*.map"
"dist/**/*.wasm"
],
"mac": {
"category": "public.app-category.developer-tools",
Expand Down
46 changes: 19 additions & 27 deletions scripts/check_bundle_size.sh
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
#!/usr/bin/env bash
# Tracks bundle sizes and fails if main.js grows too much
# Large main.js usually indicates eager imports of heavy dependencies

# Check bundle size budgets to prevent regressions
set -euo pipefail

MAIN_JS_MAX_KB=${MAIN_JS_MAX_KB:-20} # 20KB for main.js (currently ~15KB)
cd "$(dirname "$0")/.."

if [ ! -f "dist/main.js" ]; then
echo "❌ dist/main.js not found. Run 'make build' first."
exit 1
fi
# Budgets (in bytes)
MAX_INDEX_GZIP=409600 # 400KB gzipped

# Get file size (cross-platform: macOS and Linux)
if stat -f%z dist/main.js >/dev/null 2>&1; then
# macOS
main_size=$(stat -f%z dist/main.js)
else
# Linux
main_size=$(stat -c%s dist/main.js)
fi
echo "Checking bundle size budgets..."

main_kb=$((main_size / 1024))
# Find the main index bundle
INDEX_FILE=$(find dist -name 'index-*.js' | head -1)
if [[ -z "$INDEX_FILE" ]]; then
echo "❌ Error: Could not find main index bundle" >&2
exit 1
fi

echo "Bundle sizes:"
echo " dist/main.js: ${main_kb}KB (max: ${MAIN_JS_MAX_KB}KB)"
# Check index gzipped size
INDEX_SIZE=$(gzip -c "$INDEX_FILE" | wc -c | tr -d ' ')
INDEX_SIZE_KB=$((INDEX_SIZE / 1024))
MAX_INDEX_KB=$((MAX_INDEX_GZIP / 1024))

if [ $main_kb -gt $MAIN_JS_MAX_KB ]; then
echo "❌ BUNDLE SIZE REGRESSION: main.js (${main_kb}KB) exceeds ${MAIN_JS_MAX_KB}KB"
echo ""
echo "This usually means new eager imports were added to main process."
echo "Check for imports in src/main.ts, src/config.ts, or src/preload.ts"
echo ""
echo "Run './scripts/check_eager_imports.sh' to identify the issue."
echo "Main bundle (gzipped): ${INDEX_SIZE_KB}KB (budget: ${MAX_INDEX_KB}KB)"
if ((INDEX_SIZE > MAX_INDEX_GZIP)); then
echo "❌ Main bundle exceeds budget by $((INDEX_SIZE - MAX_INDEX_GZIP)) bytes" >&2
exit 1
fi

echo "βœ… Bundle size OK"
echo "βœ… Bundle size within budget"
105 changes: 92 additions & 13 deletions scripts/check_eager_imports.sh
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
#!/usr/bin/env bash
# Detects eager imports of AI SDK packages in main process
# These packages are large and must be lazy-loaded to maintain fast startup time
# Detects eager imports of heavy packages in startup-critical and renderer/worker files
#
# Main process: AI SDK packages must be lazy-loaded to maintain fast startup (<4s)
# Renderer/Worker: Large data files (models.json) and ai-tokenizer must never be imported

set -euo pipefail

# Files that should NOT have eager AI SDK imports
# Files that should NOT have eager AI SDK imports (main process)
CRITICAL_FILES=(
"src/main.ts"
"src/config.ts"
"src/preload.ts"
)

# Packages that should be lazily loaded
BANNED_IMPORTS=(
# Packages banned in main process (lazy load only)
BANNED_MAIN_IMPORTS=(
"@ai-sdk/anthropic"
"@ai-sdk/openai"
"@ai-sdk/google"
"ai"
)

# Packages banned in renderer/worker (never import)
BANNED_RENDERER_IMPORTS=(
"ai-tokenizer"
)

# Files banned in renderer/worker (large data files)
BANNED_RENDERER_FILES=(
"models.json"
)

failed=0

echo "Checking for eager AI SDK imports in critical startup files..."
echo "==> Checking for eager AI SDK imports in main process critical files..."

for file in "${CRITICAL_FILES[@]}"; do
if [ ! -f "$file" ]; then
continue
fi

for pkg in "${BANNED_IMPORTS[@]}"; do
for pkg in "${BANNED_MAIN_IMPORTS[@]}"; do
# Check for top-level imports (not dynamic)
if grep -E "^import .* from ['\"]$pkg" "$file" >/dev/null 2>&1; then
echo "❌ EAGER IMPORT DETECTED: $file imports '$pkg'"
Expand All @@ -40,8 +52,8 @@ done

# Also check dist/main.js for require() calls (if it exists)
if [ -f "dist/main.js" ]; then
echo "Checking bundled main.js for eager requires..."
for pkg in "${BANNED_IMPORTS[@]}"; do
echo "==> Checking bundled main.js for eager requires..."
for pkg in "${BANNED_MAIN_IMPORTS[@]}"; do
if grep "require(\"$pkg\")" dist/main.js >/dev/null 2>&1; then
echo "❌ BUNDLED EAGER IMPORT: dist/main.js requires '$pkg'"
echo " This means a critical file is importing AI SDK eagerly"
Expand All @@ -50,12 +62,79 @@ if [ -f "dist/main.js" ]; then
done
fi

echo "==> Checking for banned imports in renderer/worker files..."

# Find all TypeScript files in renderer-only directories
RENDERER_DIRS=(
"src/components"
"src/contexts"
"src/hooks"
"src/stores"
"src/utils/ui"
"src/utils/tokens/tokenStats.worker.ts"
"src/utils/tokens/tokenStatsCalculatorApproximate.ts"
)

for dir in "${RENDERER_DIRS[@]}"; do
if [ ! -e "$dir" ]; then
continue
fi

# Find all .ts/.tsx files in this directory
while IFS= read -r -d '' file; do
# Check for banned packages
for pkg in "${BANNED_RENDERER_IMPORTS[@]}"; do
if grep -E "from ['\"]$pkg" "$file" >/dev/null 2>&1; then
echo "❌ RENDERER IMPORT DETECTED: $file imports '$pkg'"
echo " ai-tokenizer must never be imported in renderer (8MB+)"
failed=1
fi
done

# Check for banned files (e.g., models.json)
for banned_file in "${BANNED_RENDERER_FILES[@]}"; do
if grep -E "from ['\"].*$banned_file" "$file" >/dev/null 2>&1; then
echo "❌ LARGE FILE IMPORT: $file imports '$banned_file'"
echo " $banned_file is 701KB and must not be in renderer/worker"
failed=1
fi
done
done < <(find "$dir" -type f \( -name "*.ts" -o -name "*.tsx" \) -print0)
done

# Check bundled worker if it exists
if [ -f dist/tokenStats.worker-*.js ]; then
WORKER_FILE=$(find dist -name 'tokenStats.worker-*.js' | head -1)
WORKER_SIZE=$(wc -c <"$WORKER_FILE" | tr -d ' ')

echo "==> Checking worker bundle for heavy imports..."

# If worker is suspiciously large (>50KB), likely has models.json or ai-tokenizer
if ((WORKER_SIZE > 51200)); then
echo "❌ WORKER TOO LARGE: $WORKER_FILE is ${WORKER_SIZE} bytes (>50KB)"
echo " This suggests models.json (701KB) or ai-tokenizer leaked in"

# Try to identify what's in there
if grep -q "models.json" "$WORKER_FILE" 2>/dev/null \
|| strings "$WORKER_FILE" 2>/dev/null | grep -q "anthropic\|openai" | head -10; then
echo " Found model names in bundle - likely models.json"
fi
failed=1
fi
fi

if [ $failed -eq 1 ]; then
echo ""
echo "To fix: Use dynamic imports instead:"
echo " βœ… const { createAnthropic } = await import('@ai-sdk/anthropic');"
echo " ❌ import { createAnthropic } from '@ai-sdk/anthropic';"
echo "Fix suggestions:"
echo " Main process: Use dynamic imports"
echo " βœ… const { createAnthropic } = await import('@ai-sdk/anthropic');"
echo " ❌ import { createAnthropic } from '@ai-sdk/anthropic';"
echo ""
echo " Renderer/Worker: Never import heavy packages"
echo " ❌ import { getModelStats } from './modelStats'; // imports models.json"
echo " ❌ import AITokenizer from 'ai-tokenizer'; // 8MB package"
echo " βœ… Use approximations or IPC to main process"
exit 1
fi

echo "βœ… No eager AI SDK imports detected"
echo "βœ… No banned imports detected"
Loading
Loading