How Modly Syncs Built-in Extensions from App Resources to the User Data Directory

Bold TLDR: Modly copies built-in extensions from the application bundle to <userData>/builtin-extensions at startup using syncBuiltinExtensions(), ensuring the UI always loads the latest versions while treating them identically to user-installed extensions.

When an Electron application ships with pre-bundled extensions, those files live inside the application resources where they cannot be modified at runtime. Modly solves this by syncing built-in extensions from app resources to the user data directory on every launch. This process guarantees fresh, writable copies that the UI can load and manage like any other extension, while automatically applying updates when the app version changes.

The Four-Step Sync Process

Modly's built-in extension sync follows a deterministic pipeline implemented in electron/main/builtin-sync.ts. Each step handles a specific concern: path resolution, environment detection, file operations, and lifecycle timing.

1. Determine the Destination Path

The getBuiltinExtensionsDir() function constructs the target directory using Electron's standard app.getPath('userData') API. This ensures cross-platform compatibility without hardcoding paths.

// electron/main/builtin-sync.ts#L6-L8
export function getBuiltinExtensionsDir(): string {
  return join(app.getPath('userData'), 'builtin-extensions');
}

This resolves to platform-appropriate locations:

  • macOS: ~/Library/Application Support/Modly/builtin-extensions
  • Windows: %APPDATA%/Modly/builtin-extensions
  • Linux: ~/.config/Modly/builtin-extensions

2. Locate the Source Resources

The getBuiltinResourcesDir() function distinguishes between packaged builds and development mode. This dual-path approach allows the same codebase to work during local development and in production releases.

// electron/main/builtin-sync.ts#L11-L15
export function getBuiltinResourcesDir(): string {
  if (app.isPackaged) {
    return join(process.resourcesPath, 'builtin-extensions');
  }
  return join(__dirname, '../../out/builtin-extensions');
}
  • Packaged builds: extensions ship inside Contents/Resources/builtin-extensions (macOS) or equivalent
  • Development: extensions compile to out/builtin-extensions relative to the source tree

3. Perform the Sync Operation

The syncBuiltinExtensions() function executes a destructive copy that guarantees consistency between source and destination. It implements three safety mechanisms:

Step Action Rationale
Verify source Abort with log if resources directory missing Prevents crashes on incomplete installations
Clean destination rmSync() existing builtin-extensions folder Eliminates stale files from previous versions
Recursive copy copySync() entire resources directory Creates fresh, identical copy
// electron/main/builtin-sync.ts#L32-L37
export function syncBuiltinExtensions(): void {
  const src = getBuiltinResourcesDir();
  const dest = getBuiltinExtensionsDir();

  if (!existsSync(src)) {
    console.log('No built-in extensions to sync');
    return;
  }

  // Remove stale extensions to ensure clean state
  if (existsSync(dest)) {
    rmSync(dest, { recursive: true, force: true });
  }

  // Copy fresh extensions from resources
  copySync(src, dest, { recursive: true });
  console.log('Synced built-in extensions to:', dest);
}

This always-overwrite strategy ensures updates propagate automatically. When Modly releases a new version with updated built-in extensions, users receive the changes on next launch without manual intervention.

4. Execute at Application Startup

The sync runs during Electron's app.whenReady() lifecycle, positioned strategically in electron/main/index.ts after cache cleanup but before backend initialization.

// electron/main/index.ts#L89-L103
app.whenReady().then(async () => {
  // Clear cache from previous sessions
  await clearCache();

  // Sync built-in extensions before UI or backend starts
  syncBuiltinExtensions();

  // Initialize Python backend with guaranteed fresh extensions
  const pyProcess = spawnPythonBackend();
  setupIpcHandlers();

  createMainWindow();
});

This execution order is critical. The UI loads extensions from the user data directory, not the application bundle, so the sync must complete before any renderer process attempts to enumerate available extensions.

Practical Code Examples

Trigger Sync Manually for Debugging

During development, you may need to re-sync without restarting the entire application:

import { syncBuiltinExtensions } from './builtin-sync';

// Force immediate re-sync
syncBuiltinExtensions();

Inspect Synced Extension Contents

Verify which built-in extensions are currently active in the user environment:

import { getBuiltinExtensionsDir } from './builtin-sync';
import { readdirSync } from 'fs';

const extDir = getBuiltinExtensionsDir();
console.log('Built-in extensions path:', extDir);

try {
  const extensions = readdirSync(extDir, { withFileTypes: true })
    .filter(dirent => dirent.isDirectory())
    .map(dirent => dirent.name);
  
  console.log('Installed built-in extensions:', extensions);
} catch (err) {
  console.error('No synced extensions found:', err);
}

Access Extensions in Renderer Process

The renderer receives the resolved path via IPC and loads extensions identically to user-installed ones:

// Preload or renderer process
const builtinDir = await window.electron.getBuiltinExtensionsDir();
const extensions = await loadExtensionsFromDirectory(builtinDir);

Why This Architecture Works

Modly's sync approach solves three distinct problems common to Electron extension systems:

Problem: Read-only application bundles. Electron apps on macOS are signed and cannot modify their own resources. The user data directory is always writable.

Problem: Extension hosting constraints. The extension loader expects a flat directory structure with package.json manifests. The sync produces this structure without encoding special cases for built-ins versus user installs.

Problem: Version drift on updates. Without cleaning the destination, old built-in extensions would persist alongside new ones. The rmSync + copySync pattern guarantees atomic replacement.

Summary

  • getBuiltinExtensionsDir() resolves <userData>/builtin-extensions as the writable destination
  • getBuiltinResourcesDir() selects process.resourcesPath for packaged builds or out/builtin-extensions for development
  • syncBuiltinExtensions() performs destructive copy: validates source, removes stale destination, recursively copies fresh files
  • Execution timing in electron/main/index.ts places sync between cache clear and backend start, ensuring UI sees current extensions

Frequently Asked Questions

Where are built-in extensions stored after Modly syncs them?

After sync, built-in extensions reside in the builtin-extensions subdirectory of Electron's userData folder—typically ~/Library/Application Support/Modly/builtin-extensions on macOS, %APPDATA%/Modly/builtin-extensions on Windows, and ~/.config/Modly/builtin-extensions on Linux. The getBuiltinExtensionsDir() function computes this path dynamically using app.getPath('userData').

Does Modly delete my user-installed extensions during the sync?

No. The sync operation only targets the builtin-extensions folder. User-installed extensions live in a separate directory managed by extension-install-utils.ts. The rmSync() call in syncBuiltinExtensions() is scoped strictly to the built-in destination path.

How does Modly handle built-in extension updates?

Updates are automatic and require no user action. When Modly launches after an app upgrade, the sync process detects changed source files (newer versions in process.resourcesPath), deletes the old builtin-extensions folder in user data, and copies the updated files. The destructive copy guarantees the running instance matches the application version.

Can I prevent built-in extensions from syncing to我的 user data directory?

There is no supported configuration to disable the sync. The architecture assumes built-in extensions must be writable for the extension system to function. If you need to inspect the original files, examine process.resourcesPath/builtin-extensions directly in a packaged build, though modifying these has no effect on the running application.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →