quarto-review-extension

Bundle Optimization Strategy

Current Status

Bundle Size (as of 2025-11-01):

Components:

Problem

The translation module adds significant size to the bundle:

  1. @xenova/transformers (~1.5MB) - Required for local AI translation
  2. Translation UI components (~100KB) - TranslationView, TranslationToolbar, TranslationController
  3. Translation core (~80KB) - Segmentation, alignment, state management
  4. Export service (~60KB) - Unified and separated exporters

For users who only need review functionality (comments, tracked changes, git integration), this is unnecessary overhead.

Solution: Conditional Bundling

Build Flag Strategy

Create two build targets:

  1. Full Bundle (default) - Includes translation module
  2. Lean Bundle - Review functionality only

Implementation Approach

1. Environment Variables

Add to vite.config.ts:

export default defineConfig({
  define: {
    __ENABLE_TRANSLATION__: JSON.stringify(
      process.env.ENABLE_TRANSLATION !== 'false'
    ),
  },
  // ...
});

2. Conditional Imports in src/main.ts

// Type-only imports (always available)
import type { TranslationConfig } from '@modules/translation';

// Conditional runtime import
let TranslationModule: typeof import('@modules/translation').TranslationModule | undefined;

if (__ENABLE_TRANSLATION__) {
  TranslationModule = (await import('@modules/translation')).TranslationModule;
}

export class QuartoReview {
  private translation?: InstanceType<typeof TranslationModule>;

  constructor(config: QuartoReviewConfig = {}) {
    // ...

    // Initialize translation module if enabled AND available
    if (this.config.enableTranslation && TranslationModule) {
      this.translation = new TranslationModule({
        config: translationConfig,
        changes: this.changes,
        markdown: this.markdown,
        exporter: this.exporter,
      });
    }

    // ...
  }
}

3. Dynamic UI Component Loading

// In UIModule constructor
if (config.translation && __ENABLE_TRANSLATION__) {
  // Lazy load translation UI components
  const { TranslationController } = await import(
    './translation/TranslationController'
  );
  this.translationController = new TranslationController({
    translation: config.translation,
    // ...
  });
}

4. Tree-Shaking Optimization

Ensure Vite can tree-shake unused code:

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // Separate translation into its own chunk
          if (id.includes('translation') || id.includes('@xenova')) {
            return 'translation';
          }
        },
      },
    },
  },
});

5. Build Scripts

Add to package.json:

{
  "scripts": {
    "build": "npm run build:full",
    "build:full": "vite build",
    "build:lean": "ENABLE_TRANSLATION=false vite build --mode lean",
    "build:both": "npm run build:full && npm run build:lean"
  }
}

Package Structure

Create separate extension variants:

_extensions/
├── review/              # Full version (with translation)
│   ├── _extension.yml
│   └── assets/
│       └── review.js
└── review-lean/         # Lean version (review only)
    ├── _extension.yml
    └── assets/
        └── review.js

Expected Savings

Lean Bundle (without translation)

Estimated lean bundle: ~400-500 KB (down from 2.4 MB) Estimated gzipped: ~120-150 KB (down from 533 KB)

Full Bundle (with translation)

Migration Path

Phase 1: Prepare Code (Current)

Phase 2: Add Build Flags

  1. Update vite.config.ts with environment variable
  2. Add conditional imports to main.ts
  3. Add conditional UI loading to UIModule
  4. Test both build modes

Phase 3: Optimize Dependencies

  1. Mark @xenova/transformers as optional peer dependency
  2. Add warning if translation enabled but module not available
  3. Document installation for translation features

Phase 4: Separate Bundles

  1. Create dual build pipeline
  2. Generate two extension variants
  3. Update documentation with installation options
  4. Publish both versions

Usage Examples

For Users (After Implementation)

Installing Lean Version:

quarto add username/quarto-review-lean

Installing Full Version:

quarto add username/quarto-review

Switching Versions:

# Lean version
filters:
  - review-lean

# Full version (with translation)
filters:
  - review
review:
  enabled: true
  mode: translation

Implementation Priority

High Priority (Now)

  1. Document the strategy (this file) ✅
  2. Add conditional import wrapper to main.ts
  3. Test that disabling translation works gracefully

Medium Priority (Next Release)

  1. Implement build flags in vite.config.ts
  2. Create dual build pipeline
  3. Test bundle sizes

Low Priority (Future)

  1. Separate extension variants
  2. Publish to Quarto extensions registry
  3. Add automatic bundle size monitoring

Technical Notes

TypeScript Considerations

Global type declaration for build flag:

// src/env.d.ts
declare const __ENABLE_TRANSLATION__: boolean;

Testing Strategy

Test both configurations:

// vitest.config.ts
export default defineConfig({
  test: {
    globals: true,
    setupFiles: ['./tests/setup.ts'],
    environment: 'jsdom',
  },
  define: {
    __ENABLE_TRANSLATION__: true, // Test with translation enabled
  },
});

Separate test run for lean build:

ENABLE_TRANSLATION=false npm test

Deployment Strategy

  1. GitHub Releases:
    • quarto-review-v1.0.0-full.zip (with translation)
    • quarto-review-v1.0.0-lean.zip (review only)
  2. NPM Package:
    • Separate packages: @quarto/review and @quarto/review-lean
    • Or feature flag in single package
  3. Quarto Extension Registry:
    • Two separate listings
    • Clear documentation about differences

Benefits

For End Users

For Developers

For Project

Alternative Approaches Considered

1. Lazy Loading Only

2. Separate Git Repository

3. Plugin Architecture

4. Conditional Build Flags (Chosen)

Monitoring & Metrics

Track bundle sizes over time:

// scripts/check-bundle-size.js
const fs = require('fs');
const path = require('path');

const bundlePath = path.join(__dirname, '../dist/review.js');
const stats = fs.statSync(bundlePath);
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);

console.log(`Bundle size: ${sizeMB} MB`);

if (sizeMB > 3.0) {
  console.error('⚠️  Bundle size exceeds 3 MB limit!');
  process.exit(1);
}

Add to CI/CD pipeline to alert on size regressions.

References

Timeline


Status: 📝 Planning Phase Next Action: Implement conditional import wrapper in main.ts