Skip to the content.

Overview

The logger system now supports complete removal of debug code from production builds. This follows Obsidian’s plugin guidelines that plugins should only show errors, not debugging information in the console.

How It Works

1. Build-Time Constants

The build system defines BUILD_ENV at compile time:

// esbuild.config.mjs
define: {
    'BUILD_ENV': prod ? '"production"' : '"development"'
}

2. Development Check

The logger uses this constant to check the environment:

// src/utils/Logger.ts
function isDevelopment(): boolean {
    return typeof BUILD_ENV !== 'undefined' && BUILD_ENV === 'development';
}

3. Debug Method with Guard

Debug logging is wrapped in a development check:

debug(component: ComponentName, message: string, ...args: unknown[]): void {
    // This entire block will be removed in production builds
    if (!isDevelopment()) return;
    
    if (this.shouldLog(component, 'debug')) {
        console.debug(this.formatMessage(component, message), ...args);
        // ... file logging ...
    }
}

4. Dead Code Elimination

When building for production:

  1. esbuild sets BUILD_ENV = "production"
  2. TypeScript evaluates isDevelopment() as false
  3. The if (!isDevelopment()) return; becomes if (true) return;
  4. esbuild’s tree-shaking removes all unreachable code after the return
  5. The entire debug method body is eliminated from the bundle

What Gets Removed

In Development Build

// Full debug code is included
this.logger.debug('Processing record', { id: record.id, name: record.name });
// Output: [MAIN] Processing record { id: 123, name: 'Test' }

In Production Build

// The entire debug call and all its arguments are removed!
// Not even evaluated - completely gone from the bundle
this.logger.debug('Processing record', { id: record.id, name: record.name });
// Result: This line doesn't exist in main.js at all

Benefits

Zero Runtime Overhead

Follows Obsidian Guidelines

Developer-Friendly

Safe

Usage

Development

# Build with debug code included
npm run dev

# Debug messages will appear in console

Production

# Build with debug code removed
npm run build

# Or explicitly
node esbuild.config.mjs production

# Debug messages are completely eliminated from bundle

In Your Code

import { createLogger } from '../utils/Logger';

class MyComponent {
    private logger = createLogger('myComponent');
    
    async processData(data: ComplexObject) {
        // This WILL be in production builds (info level)
        this.logger.info('Processing started');
        
        // This will be REMOVED from production builds
        this.logger.debug('Data details', { 
            data, 
            computed: someExpensiveComputation(data)
        });
        
        // Even complex debug calls are completely removed
        this.logger.debug('Complex debug', {
            // These computations won't run in production
            expensive: this.veryExpensiveOperation(),
            debug: JSON.stringify(data)
        });
        
        // Error logging always works (in production too)
        this.logger.error('Failed to process', error);
    }
}

Verification

Check Bundle Size

# Development build
npm run dev
ls -lh ../obsidian-dev-vault/.obsidian/plugins/your-plugin/main.js

# Production build  
npm run build
ls -lh ./test-vault/.obsidian/plugins/your-plugin/main.js

# Production should be significantly smaller if you have many debug statements

Check Bundle Content

# Search for debug strings in production bundle
grep -i "debug" test-vault/.obsidian/plugins/your-plugin/main.js

# You should NOT find your debug messages in production build

Test at Runtime

// In development, this works:
this.logger.debug('Test message');
// Console: [COMPONENT] Test message

// In production build, this line doesn't exist at all in the bundle

Important Notes

⚠️ What Gets Removed

Only calls to logger.debug() are removed. Other log levels remain:

⚠️ File Logging

File logging for debug messages is also removed in production builds.

⚠️ Expensive Computations

Be careful with debug arguments that have expensive computations:

// ✅ GOOD - Entire call removed in production
this.logger.debug('Data', { 
    expensive: computeExpensiveValue()
});

// ❌ AVOID - Computation happens before logger call
const expensiveData = computeExpensiveValue();
this.logger.debug('Data', expensiveData);

For expensive computations outside debug calls, wrap in development check:

import { logger } from '../utils/Logger';

if (logger.isDevelopment && logger.isDevelopment()) {
    const expensiveDebugData = computeExpensiveValue();
    this.logger.debug('Debug data', expensiveDebugData);
}

Build Output

Development Build

🔨 Building for DEVELOPMENT
📂 Target: ../obsidian-dev-vault/.obsidian/plugins/your-plugin
🐛 Debug code included

Production Build

🔨 Building for PRODUCTION
📂 Target: ./test-vault/.obsidian/plugins/your-plugin
⚡ Debug code will be removed via dead code elimination

Comparison with Other Approaches

❌ Runtime Flag (Old Approach)

// Code stays in bundle, just doesn't execute
if (this.config.debugEnabled) {
    console.debug(...); // This code is in the bundle
}

Problems:

✅ Compile-Time Elimination (New Approach)

// Code completely removed from production bundle
if (!isDevelopment()) return;
console.debug(...); // This code DOES NOT EXIST in production

Benefits:

Advanced Usage

Conditional Features

You can use the same pattern for development-only features:

// Development-only helper
if (isDevelopment()) {
    this.addRibbonIcon('bug', 'Debug Tools', () => {
        new DebugModal(this.app).open();
    });
}

Development Utilities

// Entire utility removed in production
if (isDevelopment()) {
    registerDebugCommands(this);
    enablePerformanceMonitoring(this);
}

Manual Debug Code Cleanup

In addition to automatic build-time elimination, you can permanently remove debug code from source files when features become stable.

Why Manual Cleanup?

Automatic elimination (default approach):

Manual cleanup (optional, for stable features):

Special Comment Markers

Wrap debug code blocks with special comments:

export class DataProcessor {
    processItems(items: Item[]) {
        // DEBUG_START
        this.logger.debug('Processing batch', {
            count: items.length,
            types: items.map(i => i.type),
            memory: process.memoryUsage()
        });
        // DEBUG_END
        
        for (const item of items) {
            // DEBUG_START
            this.logger.debug('Processing item', {
                id: item.id,
                type: item.type,
                data: item.data
            });
            // DEBUG_END
            
            this.process(item);
        }
    }
}

Cleanup Script Usage

The template includes a cleanup script at scripts/clean-debug-code.mjs:

# Preview what would be removed (safe, read-only)
node scripts/clean-debug-code.mjs --dry-run --all --verbose

# Remove debug blocks from specific files
node scripts/clean-debug-code.mjs src/ui/MyComponent.ts src/core/Processor.ts

# Remove debug blocks from all source files
node scripts/clean-debug-code.mjs --all

# Get help
node scripts/clean-debug-code.mjs --help

Example: Before and After Cleanup

Before cleanup (during development):

export class RecordSync {
    async syncRecord(record: Record): Promise<void> {
        // DEBUG_START
        this.logger.debug('Starting sync', {
            recordId: record.id,
            metadata: record.metadata,
            fileCount: record.files.length
        });
        // DEBUG_END
        
        try {
            const result = await this.api.upload(record);
            
            // DEBUG_START
            this.logger.debug('Upload complete', {
                recordId: record.id,
                resultId: result.id,
                duration: Date.now() - startTime
            });
            // DEBUG_END
            
            return result;
        } catch (error) {
            this.logger.error('Sync failed', error); // Stays!
            throw error;
        }
    }
}

After cleanup (stable, production-ready):

export class RecordSync {
    async syncRecord(record: Record): Promise<void> {
        try {
            const result = await this.api.upload(record);
            return result;
        } catch (error) {
            this.logger.error('Sync failed', error);
            throw error;
        }
    }
}

Much cleaner and easier to understand! 🎉

When to Use Manual Cleanup

Use automatic elimination (default):

Use manual cleanup:

# 1. Ensure feature is thoroughly tested
npm test
npm run build

# 2. Preview what would be removed
node scripts/clean-debug-code.mjs --dry-run --verbose --all

# 3. Review the output carefully
# Make sure you're not removing debugging you'll need

# 4. Run cleanup on specific files first
node scripts/clean-debug-code.mjs src/feature/MyStableFeature.ts

# 5. Test again to verify nothing broke
npm test

# 6. If all looks good, clean all files (optional)
node scripts/clean-debug-code.mjs --all

# 7. Commit with clear message
git add .
git commit -m "Clean up debug code from stable RecordSync feature

Feature has been stable for 3 months with no issues.
Removed development debug scaffolding to improve readability."

Best Practices for Debug Markers

Always use DEBUG_START/END for:

// ✅ Multi-line debug blocks you plan to eventually remove
// DEBUG_START
logger.debug('Complex state', {
    data: complexObject,
    computed: expensiveCalculation()
});
// DEBUG_END

// ✅ Single-line debug calls you plan to remove
// DEBUG_START
logger.debug('Trace point reached');
// DEBUG_END

// ✅ Debug-only code blocks
// DEBUG_START
if (isDevelopment()) {
    this.validateInternalState();
}
// DEBUG_END

Don’t use markers for:

// ❌ Error logging (permanent, should stay)
logger.error('Failed to process', error);

// ❌ Important info (permanent, should stay)
logger.info('Processing complete');

// ❌ Debug calls you want to keep for troubleshooting
logger.debug('API request started'); // No markers - keep this

Script Features

The cleanup script (scripts/clean-debug-code.mjs) provides:

Example Script Output

Debug Code Cleanup

Processing 15 files...

  ✓ src/ui/SyncModal.ts: Removed 3 debug blocks (12 lines)
  ✓ src/core/RecordProcessor.ts: Removed 5 debug blocks (23 lines)
  ○ src/settings/Settings.ts (no debug blocks)
  ✓ src/api/KadiClient.ts: Removed 2 debug blocks (8 lines)

Summary:
  Files processed:  15
  Files modified:   8
  Debug blocks:     18
  Lines removed:    67

✓ Successfully cleaned debug code from 8 files

Combining Both Approaches

You get the best of both worlds:

  1. During Development:
    • Add debug logging freely with DEBUG_START/END markers
    • Automatic elimination keeps production builds clean
    • Debug code available for troubleshooting
  2. After Stabilization:
    • Run cleanup script to remove debug scaffolding
    • Source code becomes more readable
    • Git diffs are cleaner
    • Less code to maintain
  3. Production:
    • Users never see debug code (automatic elimination)
    • Bundle size optimized
    • Console stays clean
    • Professional experience

Migration Guide

Updating Existing Plugins

  1. Copy new Logger.ts to your project
  2. Update esbuild.config.mjs with the define section
  3. Rebuild - your debug code is now automatically removed!

No need to change your existing debug calls - they automatically benefit from elimination.

Best Practices

  1. Use debug liberally during development
  2. Don’t worry about cleanup - it’s automatic
  3. Use info/warn/error for production-relevant messages
  4. Avoid expensive pre-computations before debug calls

Summary

This is a significant improvement over runtime flags and makes it safe to add extensive debug logging during development!