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:
- esbuild sets
BUILD_ENV = "production" - TypeScript evaluates
isDevelopment()asfalse - The
if (!isDevelopment()) return;becomesif (true) return; - esbuild’s tree-shaking removes all unreachable code after the return
- 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
- Debug code is completely removed, not just disabled
- No performance impact in production
- Smaller bundle size
✅ Follows Obsidian Guidelines
- No debug output in user’s console
- Only warnings and errors shown
- Professional user experience
✅ Developer-Friendly
- Debug freely during development
- No need to manually remove debug statements
- Automatic cleanup on production build
✅ Safe
- Info, warn, and error logging still works in production
- Only debug level is stripped
- File logging also respects the flag
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:
- ✅
logger.info()- Included in production - ✅
logger.warn()- Included in production - ✅
logger.error()- Included in production - ❌
logger.debug()- REMOVED from production
⚠️ 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:
- Debug code still in production bundle
- Increases bundle size
- Runtime check overhead
- Debug strings visible in source
✅ Compile-Time Elimination (New Approach)
// Code completely removed from production bundle
if (!isDevelopment()) return;
console.debug(...); // This code DOES NOT EXIST in production
Benefits:
- Zero production overhead
- Smaller bundle size
- No runtime checks
- Debug strings not in production bundle
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):
- Debug code removed from production builds ✅
- Still present in source code for future debugging ✅
- Zero impact on users ✅
Manual cleanup (optional, for stable features):
- Permanently removes debug code from source ✅
- Improves code readability ✅
- Cleaner git history and diffs ✅
- Less cognitive load when maintaining code ✅
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):
- ✅ During active development
- ✅ Debug code might be needed later
- ✅ Feature not yet stable
- ✅ Other developers might need debug info
Use manual cleanup:
- ✅ Feature is stable and well-tested
- ✅ Debug code no longer serves a purpose
- ✅ Want to improve code readability
- ✅ Before major releases
- ✅ Cleaning up after intensive debugging
Recommended Cleanup Workflow
# 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:
- ✅ Safe preview mode (
--dry-run) to see changes before making them - ✅ Selective cleaning - specify individual files
- ✅ Batch processing (
--all) for entire codebase - ✅ Detailed output (
--verbose) showing exactly what’s removed - ✅ Statistics - files processed, blocks removed, lines saved
- ✅ Validation - warns about unmatched DEBUG_START/END pairs
- ✅ Colorized output - easy to scan results
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:
- During Development:
- Add debug logging freely with DEBUG_START/END markers
- Automatic elimination keeps production builds clean
- Debug code available for troubleshooting
- After Stabilization:
- Run cleanup script to remove debug scaffolding
- Source code becomes more readable
- Git diffs are cleaner
- Less code to maintain
- Production:
- Users never see debug code (automatic elimination)
- Bundle size optimized
- Console stays clean
- Professional experience
Migration Guide
Updating Existing Plugins
- Copy new Logger.ts to your project
- Update esbuild.config.mjs with the
definesection - Rebuild - your debug code is now automatically removed!
No need to change your existing debug calls - they automatically benefit from elimination.
Best Practices
- Use debug liberally during development
- Don’t worry about cleanup - it’s automatic
- Use info/warn/error for production-relevant messages
- Avoid expensive pre-computations before debug calls
Summary
- ✅ Debug code completely removed from production builds
- ✅ Zero performance overhead in production
- ✅ Follows Obsidian plugin guidelines
- ✅ Automatic - no manual cleanup needed
- ✅ Safe - other log levels unaffected
- ✅ Smaller production bundles
This is a significant improvement over runtime flags and makes it safe to add extensive debug logging during development!