v27 Breaking Changes
v27 moves the entire electron-builder ecosystem to native ES modules, raises the minimum runtime to Node.js 22.12.0, and hard-deletes the deprecated APIs that accumulated since v22. Every breaking change in the release is catalogued on this page.
Most projects need only a Node.js bump plus one command. Run the automated migrator and follow the step-by-step migration walkthrough →.
This page is the canonical reference for what changed in v27. For the how — the ordered upgrade steps, the automated migrator, and the checklist — see the v26 → v27 migration walkthrough.
- The
build()API, all runtime configuration options, and all exported types that survive are unchanged. - CJS
require()continues to work without code changes on supported Node.js versions. - Every config-level breaking change below is rewritten automatically by
electron-builder migrate-schema(look for the Auto ✓ marker). Runtime, CLI, env-var, and behavior changes require manual action.
electron-builder migrate-schema # apply changes in place
electron-builder migrate-schema --dry-run # preview only
This handles every change marked Auto ✓ below. See the walkthrough for flags and serialization caveats.
"latest")In v27 every toolsets.* property defaults to "latest" — an unset property, null, and the literal "latest" all resolve to the newest published bundle for that toolset (previously each property defaulted to a fixed pinned version). No config change is required, but the effective defaults moved:
wine→1.0.1— Wine 11.0 (was Wine 4.0.1); macOS arm64 via Rosetta. Linux still uses host-installedwine.winCodeSign→1.3.0— Windows Kits 10.0.26100.0,osslsigncode2.11 (native arm64), and the Azure Trusted Signingdlib+ .NET 8 payload.appimage→1.1.0— static FUSE3-compatible runtime; addsunsquashfssupport.nsis(1.2.1),fpm(2.2.1),icons(1.2.1),linuxToolsMac(1.0.0), andsevenZip(1.0.0) are unchanged.
To stay on a legacy bundle, pin the toolset to "0.0.0". Because winCodeSign now defaults to 1.3.0, Azure Trusted Signing uses the faster signtool /dlib path out of the box. Full breakdown in Toolsets & environment variables.
Breaking changes at a glance
| Change | Auto | Action required |
|---|---|---|
| Node.js >=22.12.0 required | — | Update your runtime and CI Node version |
| All packages are native ESM | — | None — require() still works on Node >=22.12 |
electron-forge-maker-* are now ESM | — | None — same API, same export shape |
electronCompile removed | ✓ | Remove from config; migrate off electron-compile |
disableDefaultIgnoredFiles removed | ✓ | Removed automatically; re-include specific files via a files glob (e.g. **/*.obj) |
framework / nodeVersion / launchUiVersion removed | ✓ | Removed automatically (Electron is the only framework) |
| ProtonFramework & LibUiFramework removed | — | Migrate to an Electron-based build setup |
appImage.systemIntegration removed | ✓ | Removed automatically |
npmSkipBuildFromSource removed | ✓ | Replaced by nativeModules.buildDependenciesFromSource |
Native-module options grouped under nativeModules | ✓ | nativeRebuilder → rebuildMode |
ASAR options consolidated under asar | ✓ | asarUnpack → asar.unpack, etc.; asar: true removed |
macOS signing consolidated under mac.sign | ✓ | identity/entitlements/hardenedRuntime/… → mac.sign.*; signIgnore → sign.ignore |
mac.universal options consolidated | ✓ | mergeASARs/singleArchFiles/x64ArchFiles → mac.universal.* |
Windows signing unified under win.sign | ✓ | Discriminated union type: "signtool" | "hsm" | "pkcs11" | "azure" |
win.signExecutable / win.signAndEditExecutable removed | ✓ | false → win.sign: false; resource editing always runs |
electronDownload → electronGet | ✓ | Reshaped to @electron/get options; some legacy fields dropped (warned) |
snap config key removed | ✓ | Restructured to snapcraft with an explicit base |
Root-level directories removed | ✓ | Move under build.directories |
build.helper-bundle-id removed | ✓ | Moved to mac.helperBundleId |
squirrelWindows.noMsi removed | ✓ | Replaced by msi (inverted) |
GithubOptions.vPrefixedTagName removed | ✓ | Use tagNamePrefix |
GitlabOptions.vPrefixedTagName retained | — | None — still functional; the migrator leaves GitLab entries untouched |
devMetadata / extraMetadata in PackagerOptions removed | — | Use config / config.extraMetadata |
Implicit --publish removed | — | Pass --publish explicitly |
--em.build / --em.directories CLI flags removed | — | Use -c / -c.directories |
linux.syncDesktopName removed | — | Behaviour is always on; set desktopName to control the filename |
| Linux maintainer-script EJS syntax removed | — | Use ${var} instead of <%= var %> |
| NSIS file-association ProgID format changed | — | Update custom NSIS scripts that hard-code the old ProgID |
Toolset defaults resolve to "latest" | — | No action; pin to "0.0.0" to restore a legacy bundle |
| Toolset env-var overrides removed | — | Replace APPIMAGE_TOOLS_PATH, ELECTRON_BUILDER_NSIS_DIR, USE_SYSTEM_WINE, … with toolsets.X: { url, checksum } |
CI_BUILD_TAG env var removed | — | Use CI_COMMIT_TAG |
Azure Trusted Signing /dlib is the default | — | Pin winCodeSign below 1.3.0 only to force the legacy PowerShell path |
PlatformPackager.info & platformSpecificBuildOptions now protected | — | Plugin authors: hard break — .info no longer compiles externally; use the new pass-through getters |
Linux .desktop Exec now runs a generated *-launcher script | — | Update custom .desktop/AppArmor/MIME tooling that hard-codes the Exec command |
node_modules arch/os-filtered on every build | — | Awareness — packages whose cpu/os mismatch the target are now excluded |
Renamed type exports (ElectronDownloadOptions, WindowsAzureSigningConfiguration, …) | — | Import the new names — no compat aliases |
SnapOptions, ProtonFramework, LibUiFramework exports removed | — | Use the snapcraft config shape / Electron framework |
Runtime & ESM
Node.js >=22.12.0 required
v27 requires Node.js 22.12.0 or later. This is the version where Node's require(esm) support was stabilized (no flags needed), which allows both CJS and ESM consumers to use these packages without any code changes.
{
"engines": {
"node": ">=22.12.0"
}
}
See the walkthrough for updating your runtime, CI, and Docker images.
Native ESM output
electron-builder packages now ship as native ES modules. On Node >=22.12.0 both styles continue to work:
// CJS require() — still works
const { build, Platform } = require("electron-builder")
// ESM import — now the preferred style
import { build, Platform } from "electron-builder"
If your project uses "type": "module" or ESM imports, no changes are needed. If it uses CJS on Node >=22.12, require() continues to work as before. The build() function signature, all configuration options, and all exported types are otherwise unchanged.
moduleResolution — v27 packages include exports maps and full TypeScript declarations. "node" (legacy), "node16"/"nodenext", and "bundler" (recommended) all work.
electron-forge-maker-* plugins
The four Forge maker plugins (electron-forge-maker-appimage, electron-forge-maker-nsis, electron-forge-maker-nsis-web, electron-forge-maker-snap) are now native ES modules. The public API is identical — Electron Forge loads them via dynamic import() internally, so no config changes are required.
Removed configuration options
electronCompile
The electronCompile configuration option has been removed. electron-compile is an unmaintained library with no releases since 2019.
{ "build": { "electronCompile": true } } // ← delete this line
If your project relies on electron-compile for source compilation, migrate to a modern build tool before upgrading:
- electron-vite — recommended; fast, first-class ESM and Electron integration
- esbuild — extremely fast, minimal configuration
- webpack — mature, widely used
framework / nodeVersion / launchUiVersion
These three fields are removed. Only Electron is supported as a target framework — proton/proton-native and libui support has been removed (see Removed exports).
{
"build": {
"framework": "electron", // ← delete; "electron" was and remains the default
"nodeVersion": "current", // ← delete; no effect
"launchUiVersion": "0.1.0" // ← delete; no effect
}
}
disableDefaultIgnoredFiles
The disableDefaultIgnoredFiles option has been removed. To include a file that is excluded by default (for example a Wavefront .obj 3D model, or any other default-excluded extension/name), add an explicit files glob that targets it — an explicit include now overrides the matching default exclusion:
{
"build": {
"disableDefaultIgnoredFiles": true, // ← delete this line
"files": [
"**/*",
"**/*.obj" // ← add a glob for the default-excluded files you want to keep
]
}
}
Broad patterns such as **/* still honor the defaults; only a pattern that names the extension or directory concretely opts it back in.
appImage.systemIntegration
Removed. Desktop integration is handled automatically by AppImageLauncher.
npmSkipBuildFromSource
Removed. Use buildDependenciesFromSource (now under nativeModules). It is the logical inverse:
// v26
{ "npmSkipBuildFromSource": true }
// v27 equivalent
{ "nativeModules": { "buildDependenciesFromSource": false } }
asar: true sentinel
asar: true is no longer valid. Omit the asar key entirely to enable ASAR with defaults, or specify an object. The full ASAR restructuring is documented under ASAR options → asar.
{ "build": { "asar": true } } // Before — enable with defaults
{ "build": { "asar": {} } } // After — omit entirely (enabled by default) or use an object
Root-level directories in package.json
Specifying directories at the root of package.json is removed. Move it under the build key.
{ "name": "my-app", "directories": { "output": "dist" } } // Before
{ "name": "my-app", "build": { "directories": { "output": "dist" } } } // After
build.helper-bundle-id
The hyphenated root-level helper-bundle-id is removed. Use mac.helperBundleId.
{ "build": { "helper-bundle-id": "com.example.helper" } } // Before
{ "build": { "mac": { "helperBundleId": "com.example.helper" } } } // After
squirrelWindows.noMsi
The noMsi boolean is removed in favor of its inverse, msi.
{ "build": { "squirrelWindows": { "noMsi": true } } } // Before
{ "build": { "squirrelWindows": { "msi": false } } } // After
GithubOptions / GitlabOptions vPrefixedTagName
The vPrefixedTagName boolean on GithubOptions is removed. Use tagNamePrefix to control the tag prefix.
{ "publish": { "provider": "github", "vPrefixedTagName": false } } // Before
{ "publish": { "provider": "github", "tagNamePrefix": "" } } // After (empty string = no prefix)
To keep the default v prefix, simply remove vPrefixedTagName — tagNamePrefix defaults to "v".
GitlabOptions.vPrefixedTagName is not removedOnly the GitHub field was removed. On GitLab, vPrefixedTagName is unchanged in v27 — it still exists in the type, the schema, and the runtime, and continues to control the tag prefix (vPrefixedTagName: false → 1.2.3; omit it → v1.2.3). It has no tagNamePrefix equivalent, so migrate-schema leaves GitLab publish entries untouched. No action is required.
linux.syncDesktopName (always synced)
The linux.syncDesktopName flag is removed. The behaviour it gated is now always on: the installed .desktop filename is always derived from the desktopName field in package.json — installed as ${desktopName}.desktop, with any trailing .desktop in the value stripped first — falling back to executableName only when desktopName is absent.
// Before (v26) — opt-in
{ "build": { "linux": { "syncDesktopName": true } } } // ← delete this line
No replacement is needed — simply remove the flag. To control the installed filename, set (or omit) desktopName in your root package.json.
Why this changed: Electron derives its app_id / WM_CLASS from desktopName, and desktop environments match a running window to its launcher entry by comparing WM_CLASS against the installed .desktop filename. When the two diverged — which is exactly what happened with syncDesktopName: false (the v26 default) whenever a desktopName was set — GNOME, KDE, and others failed to associate the window with its launcher, breaking taskbar grouping, dock icons, and launcher highlighting (see #9103). Removing the flag eliminates a footgun rather than removing functionality. Path-traversal/NUL validation on the resulting filename is unchanged.
Linux maintainer-script EJS template syntax
The legacy <%= varName %> EJS interpolation in Linux maintainer scripts (e.g. FPM after-install/after-remove) is removed. Use shell-style ${varName} instead.
devMetadata / extraMetadata (programmatic PackagerOptions)
These fields in the programmatic PackagerOptions API are removed (they have thrown InvalidConfigurationError since v22).
// Before
await build({ targets: Platform.MAC.createTarget(), devMetadata: { … }, extraMetadata: { … } })
// After
await build({ targets: Platform.MAC.createTarget(), config: { extraMetadata: { … } } })
Restructured configuration
Native-module options → nativeModules
Four root-level configuration properties are moved into a new nativeModules sub-key, and nativeRebuilder is renamed to rebuildMode.
// Before (v26)
{ "build": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "nativeRebuilder": "parallel" } }
// After (v27)
{ "build": { "nativeModules": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "rebuildMode": "parallel" } } }
npmArgs is not affected — it controls the package-manager install phase and remains at the root level. npmSkipBuildFromSource (deprecated in v26) is removed; migrate-schema converts it to its inverse, nativeModules.buildDependenciesFromSource.
ASAR options → asar
All ASAR-related configuration is now nested under a single asar key. Flat root-level properties are removed. The asar type is now AsarOptions | false | null.
| Removed key | Replacement |
|---|---|
asar-unpack | asar.unpack |
asar-unpack-dir | asar.unpack |
asar.unpackDir | asar.unpack |
asarUnpack | asar.unpack |
disableSanityCheckAsar | asar.disableSanityCheck |
disableAsarIntegrity | asar.disableIntegrity |
asar: true | (removed — absence means enabled) |
// Before
{ "build": { "asarUnpack": ["**/*.node"], "disableSanityCheckAsar": true, "disableAsarIntegrity": true } }
// After
{
"build": {
"asar": {
"unpack": ["**/*.node"],
"disableSanityCheck": true,
"disableIntegrity": true
}
}
}
When asar: false, all the sub-options are irrelevant and the migrator skips them.
macOS signing → mac.sign
All macOS code-signing options now live inside a single sign object on mac (and mas / masDev), mirroring the Windows win.sign grouping. mac.sign is typed as CustomMacSign | ElectronSignOptions | string | null, where ElectronSignOptions is a typed pass-through to @electron/osx-sign.
Removed (mac.*) | Replacement (mac.sign.*) |
|---|---|
identity | sign.identity |
entitlements | sign.entitlements |
entitlementsInherit | sign.entitlementsInherit |
entitlementsLoginHelper | sign.entitlementsLoginHelper |
provisioningProfile | sign.provisioningProfile |
type | sign.type |
binaries | sign.binaries |
requirements | sign.requirements |
hardenedRuntime | sign.hardenedRuntime |
gatekeeperAssess | sign.gatekeeperAssess |
strictVerify | sign.strictVerify |
preAutoEntitlements | sign.preAutoEntitlements |
timestamp | sign.timestamp |
additionalArguments | sign.additionalArguments |
signIgnore | sign.ignore (renamed to the @electron/osx-sign canonical name) |
// Before
{ "mac": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "signIgnore": ["**/*.txt"] } }
// After
{ "mac": { "sign": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "ignore": ["**/*.txt"] } } }
To skip signing, use mac.sign.identity: null (or mac.sign: null). The same structure applies to mas and masDev.
If you used mac.sign as a custom signing function or module path (sign: "./customSign.js") together with sibling fields like identity, the two can no longer coexist — sign is now a single union. migrate-schema leaves your custom signer untouched and prints a warning so you can decide whether to keep the custom function or switch to an ElectronSignOptions object.
mac.universal
The universal-build options move from mac root into a universal object (typed as ElectronUniversalOptions, a pass-through to @electron/universal). They have no effect unless the target arch is universal.
Removed (mac.*) | Replacement (mac.universal.*) |
|---|---|
mergeASARs | universal.mergeASARs |
singleArchFiles | universal.singleArchFiles |
x64ArchFiles | universal.x64ArchFiles |
{ "mac": { "mergeASARs": true, "singleArchFiles": "*.node" } } // Before
{ "mac": { "universal": { "mergeASARs": true, "singleArchFiles": "*.node" } } } // After
Windows signing → win.sign
In v26, Windows signing used two separate root-level keys (signtoolOptions and azureSignOptions). In v27, all Windows signing modes are expressed through a single win.sign key typed as a discriminated union:
win.sign: { type: "signtool" | "hsm" | "pkcs11" | "azure", … } | false | null
false / null disables signing entirely. Unset means electron-builder discovers credentials from the environment (e.g. WIN_CSC_LINK).
win.signtoolOptions → win.sign: { type: "signtool", … } — all fields move verbatim; add type: "signtool".
{ "win": { "signtoolOptions": { "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // Before
{ "win": { "sign": { "type": "signtool", "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // After
win.azureSignOptions → win.sign: { type: "azure", … } — fields move into win.sign with type: "azure". The old index signature ([k: string]: string) that allowed arbitrary extra keys is replaced by an explicit additionalMetadata object. migrate-schema both restructures the key and moves any unrecognized extra keys into additionalMetadata.
// Before
{ "win": { "azureSignOptions": { "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "ExcludeCredentials": "ManagedIdentityCredential" } } }
// After
{ "win": { "sign": { "type": "azure", "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "additionalMetadata": { "ExcludeCredentials": "ManagedIdentityCredential" } } } }
New signing modes (beta): hsm and pkcs11
v27 introduces two new signing modes in win.sign:
type: "hsm"— Hardware Security Module via signtool/csp /kc(Windows-only; requirestoolsets.winCodeSign: "1.x").type: "pkcs11"— PKCS#11 token via osslsigncode (cross-platform; runs on macOS/Linux CI without a Windows VM).
{
"win": {
"sign": {
"type": "pkcs11",
"pkcs11Module": "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
"pkcs11KeyUri": "pkcs11:token=MyToken;object=MyKey;type=private",
"certificateFile": "cert.pem"
}
},
"toolsets": { "winCodeSign": "1.3.0" }
}
Both HSM and PKCS#11 are beta — the interfaces are stable but real-hardware test coverage is limited.
win.signExecutable / win.signAndEditExecutable removed
| Removed | Replacement |
|---|---|
win.signExecutable: false | win.sign: false (disables signing; resource editing still runs) |
win.signExecutable: true | (deleted — signing is enabled by default when credentials are available) |
win.signAndEditExecutable: true | (deleted — resource editing always runs) |
win.signAndEditExecutable: false | No direct equivalent — see note below |
win.signAndEditExecutable: false formerly skipped both resource editing (icon, metadata) and signing. In v27 resource editing always runs. To skip only signing, use win.sign: false. If you need to skip resource editing for a specific artifact, apply resources manually after building.
electronDownload → electronGet
The electronDownload configuration key is renamed to electronGet and reshaped to match @electron/get's options directly (v27 upgrades to @electron/get v5, which downloads via fetch).
Old electronDownload.* | New electronGet.* |
|---|---|
mirror | mirrorOptions.mirror |
isVerifyChecksum: false | unsafelyDisableChecksums: true |
cache, customDir, customFilename, strictSSL | (no equivalent — dropped by migrate-schema with a warning) |
{ "electronDownload": { "mirror": "https://my-mirror/" } } // Before
{ "electronGet": { "mirrorOptions": { "mirror": "https://my-mirror/" } } } // After
snap → snapcraft
The top-level snap configuration key is removed. Use snapcraft with an explicit base field and per-base options nested under a sub-key named after the base.
// Before
{ "build": { "snap": { "confinement": "strict", "stagePackages": ["libfoo"], "base": "core22" } } }
// After
{ "build": { "snapcraft": { "base": "core22", "core22": { "confinement": "strict", "stagePackages": ["libfoo"] } } } }
Supported base values: "core18", "core20", "core22", "core24", and "custom". migrate-schema performs this restructuring; when the old config has no base, it assumes "core20" and warns so you can confirm. A base: "custom" config (inline/path snapcraft.yaml) is moved verbatim. The SnapOptions export is also removed — see Removed exports.
CLI changes
Implicit --publish removed
v27 no longer auto-publishes based on the presence of CI tag environment variables, git tags, or npm lifecycle events. Pass --publish <always|onTag|onTagOrDraft|never> explicitly in your release scripts, or set the publish option in your configuration.
Why: unexpected auto-publishing could accidentally expose secrets or publish unfinished work. Making publishing explicit closes that hole.
Removed flags: --em.build, --em.directories
These flags are removed (they have thrown since v22).
| Removed flag | Replacement |
|---|---|
--em.build | -c (pass build config inline) |
--em.directories | -c.directories |
New command: migrate-schema
v27 adds electron-builder migrate-schema, which rewrites your config to v27 form in place and auto-migrates static (json/json5/yaml/package.json) and programmatic (.js/.ts/.cjs/.mjs) configs. See the walkthrough.
Toolsets & environment variables
Toolset defaults resolve to "latest" (newest bundle)
In v27 every toolsets.* property defaults to "latest" — an unset property, null, or the literal "latest" all resolve to the newest published bundle for that toolset. (Earlier v27 prereleases pinned a fixed default per toolset; those fixed defaults are gone.) The null value is no longer part of the ToolsetConfig type — it still works at runtime, but TypeScript/programmatic configs typed against Configuration should switch null → "latest" or omit the key. migrate-schema does not rewrite this.
| Toolset | v26 default | v27 "latest" resolves to | What the upgrade entails |
|---|---|---|---|
wine | 0.0.0 (Wine 4.0.1, macOS only) | 1.0.1 | Wine 11.0; macOS arm64 via Rosetta. Linux uses host-installed wine (no bundle shipped) |
winCodeSign | 0.0.0 (winCodeSign 2.6.0) | 1.3.0 | Windows Kits 10.0.26100.0; osslsigncode 2.11 + native arm64; bundles the Azure Trusted Signing dlib + .NET 8 runtime |
appimage | 0.0.0 (FUSE2 runtime) | 1.1.0 | Static FUSE3-compatible runtime (runs without a host FUSE install); adds unsquashfs support |
nsis | 0.0.0 (NSIS 3.0.4.1, split bundle) | 1.2.1 | NSIS 3.12; unified single-archive bundle; entrypoint scripts auto-set NSISDIR |
fpm | 2.2.1 | 2.2.1 | Unchanged — FPM 1.17.0 / Ruby 3.4.3 |
icons | 1.2.1 | 1.2.1 | Unchanged — wasm-vips + @resvg/resvg-wasm |
linuxToolsMac | 1.0.0 | 1.0.0 | Unchanged — gnu-tar, lzip, binutils, etc. (macOS → Linux archives) |
sevenZip | 1.0.0 | 1.0.0 | Unchanged — only published version |
No action required for most projects — the new bundles are drop-in replacements and produce identical output. If you hit a regression introduced by a newer bundle, pin back by setting the toolset version to "0.0.0":
{ "build": { "toolsets": { "winCodeSign": "0.0.0", "nsis": "0.0.0", "appimage": "0.0.0", "wine": "0.0.0" } } }
This escape hatch is intended as a short-term workaround. The "0.0.0" alias may be removed in a future major release.
Toolset env-var overrides removed
This is a breaking change if you used env-var toolset overrides. The following environment variables are removed — replace each with a ToolsetCustom object on the relevant toolsets key:
| Removed env var | Toolset it controlled |
|---|---|
APPIMAGE_TOOLS_PATH | AppImage build tools (mksquashfs, runtime) |
LINUX_TOOLS_MAC_PATH | Linux-tools-mac bundle (ar, lzip, gtar) |
CUSTOM_FPM_PATH | FPM executable |
ELECTRON_BUILDER_NSIS_DIR | NSIS compiler bundle directory |
ELECTRON_BUILDER_NSIS_RESOURCES_DIR | NSIS resources/plugins directory |
CUSTOM_NSIS_RESOURCES | Alternate NSIS resources bundle |
ELECTRON_BUILDER_WINE_TOOLSET_DIR | Wine bundle directory |
USE_SYSTEM_WINE | Forced the host-installed Wine instead of the downloaded bundle |
USE_SYSTEM_SIGNCODE | Forced the host signtool/signcode instead of the bundled winCodeSign toolset |
USE_SYSTEM_OSSLSIGNCODE | Forced the host osslsigncode instead of the bundled one |
The three USE_SYSTEM_* variables above have no env-var replacement — configure signing through win.sign and the winCodeSign toolset instead. (USE_SYSTEM_FPM is unchanged and still works.)
The url accepts an https:// URL (downloaded and cached automatically) or a file:// path (used as-is). The bundle must mirror the directory layout of the corresponding built-in bundle (see electron-builder-binaries/packages).
// Remote bundle (URL)
{ "build": { "toolsets": { "nsis": { "url": "https://example.com/my-nsis-bundle-1.0.tar.gz", "checksum": "sha256:abc123…", "version": "my-custom-1.0" } } } }
// Local directory (no checksum required)
{ "build": { "toolsets": { "appimage": { "url": "file:///path/to/my-appimage-tools-dir" } } } }
Wine note: with
USE_SYSTEM_WINEgone, Linux uses the host-installedwineby default (no bundle is shipped for Linux), and macOS uses the downloaded Wine 11.0 bundle. To point at a custom Wine build, supply aToolsetCustomobject ontoolsets.wine.
Supported archive formats: .zip, .7z, .tar.gz, .tar.xz. Exception for sevenZip: because 7-Zip is used to extract .7z and .tar.xz archives, a custom sevenZip bundle can only be supplied as a .tar.gz, .zip, or bare file:// directory.
CI_BUILD_TAG environment variable
Removed. Use CI_COMMIT_TAG (the standard GitLab CI variable) to provide the release tag.
Azure Trusted Signing signtool /dlib is the default
Azure Trusted Signing (win.sign: { type: "azure" }) uses the faster signtool /dlib path automatically: the default winCodeSign ("latest" → 1.3.0) ships the ATS dlib + .NET 8 payload, so no pin is required. To force the legacy PowerShell Invoke-TrustedSigning path instead (which requires the TrustedSigning PS module in the signing environment), pin winCodeSign below 1.3.0:
{ "build": { "toolsets": { "winCodeSign": "1.2.1" } } } // or "0.0.0" — forces the legacy PowerShell signing path
The signer resolves the path from the winCodeSign value: unset / null / "latest" and any explicit version >= 1.3.0 use signtool /dlib; a version below 1.3.0 (no dlib in the bundle) uses PowerShell. A ToolsetCustom object uses dlib from your supplied bundle.
NSIS & behavior changes
NSIS file-association ProgID format changed
NSIS installers that declare fileAssociations now register each association under a unique, Microsoft-recommendation-compliant ProgID instead of using the association name (or extension) verbatim. The previous value could collide with unrelated applications — most easily when forking a project without changing fileAssociations[].name.
The generated ProgID has the form <program>.<component>: <program> is derived from your productName (so it stays readable in the registry) and <component> mixes a short readable prefix with a value derived from the app GUID. It is at most 39 characters, contains only letters, digits, and a single period, and never starts with a digit.
No config change is required, and there is nothing to auto-migrate. fileAssociations and its name / ext / description fields are unchanged; the new ProgID is produced automatically. The installer and uninstaller derive the same ProgID, so registration and removal stay in sync.
Action is required only if you ship a custom NSIS script (nsis.include / nsis.script) or external tooling that hard-codes the old ProgID — the association name or extension — for example to add extra shell verbs or registry entries under that key. Update those references to the new generated value.
On upgrade, an installer built with v27 registers the new ProgID. One-click installers run the previous version's uninstaller during the upgrade, which removes the old-format entry; with assisted installers that do not uninstall the prior version first, the old ProgID may remain in the registry until that version is removed.
Linux launcher entrypoint
Every Linux target (deb/rpm, AppImage, snap, flatpak) now launches through a generated <executableName>-launcher shell script rather than invoking the executable directly. Two things change in the built artifact:
- The package ships a new file —
<executableName>-launcher(e.g./opt/<app>/MyApp-launcher). - The generated
.desktopExeckey points at the launcher instead of the executable (e.g.Exec=/opt/MyApp/MyApp-launcher %U), andexecutableArgs/forceX11flags are injected into the launcher rather than inlined intoExec.
This makes executableArgs apply consistently across all Linux targets and keeps the .desktop Exec a plain command.
Action is required only if you ship a custom .desktop override, an AppArmor/snap profile, a MIME handler, or external tooling that hard-codes the Exec command or assumes the executable itself is the launch target. Point those at the *-launcher script (or the executable, as appropriate).
node_modules are now arch/os-filtered on every build
v27 filters node_modules by each package's package.json cpu / os fields against the target arch and platform on every build (previously this effectively only mattered for universal macOS builds). A dependency that declares an incompatible cpu/os for the target is excluded from the packaged app, whereas v26 copied host-installed node_modules verbatim.
No action is required for typical projects — this fixes universal-build failures and produces correctly-scoped output. Be aware of it only if you intentionally bundled a cross-arch or cross-os optional binary that is now dropped; in that rare case, include it explicitly via extraResources / files.
Programmatic & plugin-author API changes
This section only affects you if you import from
app-builder-liband access thepackagerorplatformPackagerobjects directly. Standard project configurations are unaffected.
info: Packager is now protected
PlatformPackager exposed a public info: Packager field in v26. In v27 it is protected — a hard compile break, not a soft deprecation: external code that chains through .info. (a custom target, or a plugin importing app-builder-lib) no longer type-checks. All commonly-needed properties are now directly accessible on PlatformPackager — drop the .info. chain:
| Before (deprecated) | After |
|---|---|
packager.info.tempDirManager | packager.tempDirManager |
packager.info.metadata | packager.metadata |
packager.info.framework | packager.framework |
packager.info.cancellationToken | packager.cancellationToken |
packager.info.repositoryInfo | packager.repositoryInfo |
packager.info.relativeBuildResourcesDirname | packager.relativeBuildResourcesDirname |
packager.info.stageDirPathCustomizer | packager.stageDirPathCustomizer |
packager.info.areNodeModulesHandledExternally | packager.areNodeModulesHandledExternally |
packager.info.isPrepackedAppAsar | packager.isPrepackedAppAsar |
packager.info.appDir | packager.appDir |
packager.info.getWorkspaceRoot() | packager.getWorkspaceRoot() |
packager.info.emitArtifactBuildStarted(e) | packager.emitArtifactBuildStarted(e) |
packager.info.emitArtifactBuildCompleted(e) | packager.emitArtifactBuildCompleted(e) |
packager.info.emitArtifactCreated(e) | packager.emitArtifactCreated(e) |
packager.info.emitMsiProjectCreated(p) | packager.emitMsiProjectCreated(p) |
packager.info.emitAppxManifestCreated(p) | packager.emitAppxManifestCreated(p) |
The getters already on PlatformPackager in v26 are unchanged (config, projectDir, buildResourcesDir, packagerOptions, appInfo, debugLogger).
platformSpecificBuildOptions is now protected
External consumers should use the two new public helpers instead:
| Access pattern | v27 replacement |
|---|---|
packager.platformSpecificBuildOptions (direct read) | packager.platformOptions |
deepAssign({}, packager.platformSpecificBuildOptions, config.X) | packager.getOptionsForTarget<T>("X") |
platformOptions is a typed getter returning the same platform-level config object. getOptionsForTarget<T>(key) performs the standard merge of platform options with the named per-target key (e.g. "appx", "msi") and returns the result as T. All built-in targets have been migrated; custom targets extending PlatformPackager must update any direct .platformSpecificBuildOptions access.
Removed exports
ProtonFramework, LibUiFramework, and SnapOptions are removed from the public exports of app-builder-lib and electron-builder. Use the Electron framework and the snapcraft config shape respectively. The removed framework support means framework: "proton" | "libui" no longer has any effect — see framework removed.
Renamed type exports
These exported TypeScript types were renamed with no compatibility alias — import { OldName } is now a hard compile error. Update the import to the new name:
| v26 export | v27 export |
|---|---|
ElectronDownloadOptions | ElectronGetOptions |
WindowsAzureSigningConfiguration | WindowsAzureSigningConfig |
WindowsSigntoolConfiguration | WindowsSigntoolSigningConfig |
ElectronGetOptions is re-exported from the top-level electron-builder package, so this affects ordinary TypeScript consumers, not just app-builder-lib plugin authors.
Design notes
Rationale behind the user-facing breaking changes, for those who want full transparency.
Why native ESM?
electron-builder historically shipped CommonJS. The move to native ESM was driven by the ecosystem: major dependencies (chalk, figures, ora, etc.) have dropped CJS support, and Node.js 22.12's stabilized require(esm) makes it safe to ship ESM without breaking CJS consumers. The minimum was set to 22.12.0 specifically because earlier Node.js versions require --experimental-require-module to require() an ESM package.
Why move native-module options under nativeModules?
In v26, options like buildDependenciesFromSource, nodeGypRebuild, npmRebuild, and nativeRebuilder were scattered at the root of Configuration alongside unrelated properties. They were grouped under nativeModules for the same reason directories and toolsets are sub-keys: related options should be co-located. The rename nativeRebuilder → rebuildMode makes the field name consistent with other enum-style selectors.
Why unify all Windows signing under win.sign?
In v26, Windows signing was split across two sibling root-level keys — signtoolOptions and azureSignOptions — with precedence rules that were easy to misconfigure (both present → Azure wins, silently). v27 replaces them with a single win.sign key typed as a discriminated union. This makes the active signing mode explicit, self-documenting in IntelliSense, and mirrors the macOS mac.sign shape so both platforms have the same mental model. The win.sign: false sentinel explicitly disables signing, distinguishing it from "not configured" (env-based discovery).
Why replace [k: string] in WindowsAzureSigningConfiguration?
The v27 Azure integration switches from PowerShell Invoke-TrustedSigning to signtool.exe /dlib /dmdf, which reads a metadata.json file. An explicit additionalMetadata: Record<string, string> field is cleaner than an index signature because it makes intent visible in IntelliSense and prevents accidental shadowing of the typed fields. The legacy PowerShell path remains available (triggered when toolsets.winCodeSign is pinned below "1.3.0").
Why restructure snap into snapcraft?
The snapcraft shape makes the base explicit and nests per-base options under a base-named sub-key, so a single config can describe multiple bases unambiguously and core24 (which uses the snapcraft CLI directly) can coexist with legacy bases. migrate-schema automates the move but assumes core20 when no base is present, because that is the v27 1-to-1 migration target — verify the assumption for your project.
Why consolidate macOS signing under mac.sign?
In v26, ~15 signing options were scattered across the root of MacConfiguration intermixed with unrelated packaging options. Grouping them under a single sign object makes the signing surface self-documenting and matches the Windows win.sign grouping. sign is now a typed pass-through (ElectronSignOptions) to @electron/osx-sign, so upstream options are forwarded directly and new osx-sign fields are picked up automatically. signIgnore was renamed to sign.ignore to match the osx-sign canonical name. The same grouping rationale produced mac.universal (a pass-through to @electron/universal).
Why rename electronDownload to electronGet?
v27 upgrades to @electron/get v5, which downloads via fetch (replacing the got-based path) and exposes a mirrorOptions object rather than the old flat mirror/customDir fields. Renaming the config key to electronGet and typing it directly as the library's own options removes electron-builder's hand-maintained translation layer — what you set is what @electron/get receives. A few legacy fields (cache, customDir, customFilename, strictSSL) have no v5 equivalent; migrate-schema drops them with a warning.
Why consolidate ASAR options under asar?
In v26, asarUnpack lived alongside asar, and disableSanityCheckAsar / disableAsarIntegrity lived at the root of Configuration. This was confusing: asarUnpack is meaningless when asar: false, yet nothing in the type system expressed that relationship. Moving all options under a single asar key makes the dependency explicit. The asar: true sentinel was removed because having both true (enable with defaults) and {} (same meaning) was redundant.
PlatformPackager.info is now protected
The public info: Packager field created a hard coupling: any internal Packager refactor became a breaking change for all plugins. In v27, direct pass-through getters and methods were added for the properties plugin authors actually need, and info was changed from public to protected. This is a hard break in v27 (not a deferred deprecation): external packager.info.X access no longer compiles, so migrate to the direct getters listed above.
Internal changes (non-user-facing)
These changes have no impact on your build configuration or the public API. They are listed for transparency about what shipped in v27.
- Native ESM build pipeline. Babel was removed entirely; packages are compiled by
tscto native ESM withexportsmaps and full type declarations. - Code-quality modernization. Production code paths adopt native Node.js APIs and modern TypeScript patterns (
sleep()fromtimers/promises, native process signal handlers,Error.cause,Reflect.get/set, the WHATWGURLAPI, SHA-256 for WiX directory-ID generation). - Dead-code removal.
ProtonFramework,LibUiFramework, andbinDownload.tswere deleted; all binary downloads were consolidated into a singledownloadBuilderToolsetpath. - Flags consolidation. All boolean
process.envflags were consolidated into a singleflags.ts, andvalidateShellEmbeddablemoved tobuilder-util/envUtil. - Source reorganization. Platform-specific files were split into per-platform subdirectories. The public API surface is unchanged.
- Test suite. Duplicated test files were replaced by runtime-generated tests that fan out across toolset version combinations.
@electron/*dependency major bumps.@electron/get3→5 (nowfetch-based; drives theelectronGetrename),@electron/osx-sign1→2 and@electron/universal2→3 (drive themac.sign/mac.universalpass-throughs), plus@electron/asar3→4,@electron/notarize2→3, and@electron/fuses1→2. The fuses bump adds an optionalwasmTrapHandlers?: booleanfield toelectronFuses; no existing fuse field was removed.