electron-builder 27.0.0-alpha.0 → 27.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assert-node-version.js +39 -0
- package/cli.js +5 -1
- package/dist/builder.d.ts +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +43 -24
- package/dist/builder.js.map +1 -1
- package/dist/cli/clear-cache.d.ts.map +1 -1
- package/dist/cli/clear-cache.js +4 -2
- package/dist/cli/clear-cache.js.map +1 -1
- package/dist/cli/cli-util.d.ts.map +1 -1
- package/dist/cli/cli-util.js +3 -1
- package/dist/cli/cli-util.js.map +1 -1
- package/dist/cli/cli.js +7 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/create-self-signed-cert.js +3 -3
- package/dist/cli/create-self-signed-cert.js.map +1 -1
- package/dist/cli/create-update-key.d.ts +2 -0
- package/dist/cli/create-update-key.d.ts.map +1 -0
- package/dist/cli/create-update-key.js +34 -0
- package/dist/cli/create-update-key.js.map +1 -0
- package/dist/cli/install-app-deps.js +2 -6
- package/dist/cli/install-app-deps.js.map +1 -1
- package/dist/cli/migrate-schema-programmatic.d.ts +26 -0
- package/dist/cli/migrate-schema-programmatic.d.ts.map +1 -0
- package/dist/cli/migrate-schema-programmatic.js +1581 -0
- package/dist/cli/migrate-schema-programmatic.js.map +1 -0
- package/dist/cli/migrate-schema.d.ts +27 -0
- package/dist/cli/migrate-schema.d.ts.map +1 -1
- package/dist/cli/migrate-schema.js +737 -49
- package/dist/cli/migrate-schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/indexInternal.d.ts +1 -0
- package/dist/indexInternal.js +1 -1
- package/dist/publish.d.ts +1 -1
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +27 -17
- package/dist/publish.js.map +1 -1
- package/install-app-deps.js +5 -1
- package/package.json +7 -6
|
@@ -1,11 +1,116 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
2
|
+
import { AZURE_KNOWN_FIELDS, ELECTRON_DOWNLOAD_DROPPED, formatLegacyOptionMessage, MAC_SIGN_FIELDS, MAC_SIGN_REMOVED_FIELDS, MAC_UNIVERSAL_FIELDS, NSIS_CONFIG_KEYS, RESOLVED_LEGACY_CONFIG_OPTIONS, } from "app-builder-lib/internal";
|
|
3
|
+
import { log, orNullIfFileNotExist } from "builder-util";
|
|
3
4
|
import { promises as fs } from "fs";
|
|
4
5
|
import * as path from "path";
|
|
6
|
+
import { loadTypeScript, migrateProgrammaticSource } from "./migrate-schema-programmatic.js";
|
|
5
7
|
const _require = createRequire(import.meta.url);
|
|
6
8
|
// ─── Pure migration logic ─────────────────────────────────────────────────────
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
+
// The v26 -> v27 key mapping is owned by app-builder-lib so the runtime guard
|
|
10
|
+
// (`checkLegacyConfiguration`) and this migrator cannot drift apart. Re-exported here because these
|
|
11
|
+
// names are part of this module's existing public surface.
|
|
12
|
+
export { AZURE_KNOWN_FIELDS, ELECTRON_DOWNLOAD_DROPPED, MAC_SIGN_FIELDS, MAC_SIGN_REMOVED_FIELDS, MAC_UNIVERSAL_FIELDS } from "app-builder-lib/internal";
|
|
13
|
+
// Advisory surfaced (informational only — never rewrites the config) when a project builds an nsis-web target. As of v27,
|
|
14
|
+
// electron-updater rejects web-installer updates unless disableWebInstaller is false (v27+ nsis-web installs opt in automatically).
|
|
15
|
+
export const NSIS_WEB_ADVISORY = "nsis-web target detected. In v27, electron-updater rejects web-installer updates (ERR_UPDATER_WEB_INSTALLER_DISABLED) unless autoUpdater.disableWebInstaller is false; " +
|
|
16
|
+
"installs made by a v27+ nsis-web installer opt in automatically, so set autoUpdater.disableWebInstaller = false in your main process (a runtime setting, not a build-config key) " +
|
|
17
|
+
"for installs made by an older installer or when switching from nsis to nsis-web. " +
|
|
18
|
+
"The nsis-web installer also verifies a --package-file package, and a package downloaded from the URL derived from the publish configuration (no nsisWeb.appPackageUrl), against its built-in hashes; " +
|
|
19
|
+
"set nsisWeb.allowUnverifiedAppPackage: true only if one installer is used with packages of other builds. " +
|
|
20
|
+
"See https://www.electron.build/docs/migration/v27-breaking-changes#disablewebinstaller-defaults-to-true";
|
|
21
|
+
// Advisory surfaced when nsis/nsisWeb installs per-machine. v27 sets isAdminRightsRequired in the update info of per-machine builds, so
|
|
22
|
+
// electron-updater starts those updates with elevate.exe and skips them in the automatic install at launch.
|
|
23
|
+
export const NSIS_PER_MACHINE_UPDATE_ADVISORY = "nsis/nsisWeb perMachine: true detected. In v27, electron-updater starts per-machine updates with elevate.exe directly and does not install them automatically at launch " +
|
|
24
|
+
'with autoInstallEvent "onNextLaunch" (call autoUpdater.installPendingUpdateIfAvailable()). ' +
|
|
25
|
+
"See https://www.electron.build/docs/migration/v27-breaking-changes#nsis-per-machine-builds-set-isadminrightsrequired";
|
|
26
|
+
// Advisory surfaced when a custom win.sign.sign hook has no publisherName. v27 fails such a build when it writes app-update.yml.
|
|
27
|
+
export const WIN_SIGN_HOOK_PUBLISHER_NAME_ADVISORY = "win.sign.sign (custom signing hook) without win.sign.publisherName detected. In v27, a signed build that writes app-update.yml " +
|
|
28
|
+
"(an nsis, nsis-web or electronUpdaterAware appx target with a publish configuration, including one inferred from a GitHub repository) fails with InvalidConfigurationError " +
|
|
29
|
+
"because electron-builder cannot tell which certificate the hook signs with (a certificate in the config or from WIN_CSC_LINK / CSC_LINK doesn't supply the publisher name of a hook). " +
|
|
30
|
+
"Set win.sign.publisherName to the subject of the certificate your hook signs with, or win.verifyUpdateCodeSignature: false only if your updates are not Authenticode-signed. " +
|
|
31
|
+
"See https://www.electron.build/docs/migration/v27-breaking-changes#windows-publishername-is-validated-against-the-signing-certificate";
|
|
32
|
+
// Advisory surfaced when a mac/mas/masDev config does not name its own entitlements file. v27 stopped granting
|
|
33
|
+
// allow-unsigned-executable-memory and disable-library-validation in the bundled default, which only fails at runtime.
|
|
34
|
+
export const MAC_ENTITLEMENTS_ADVISORY = "In v27 the bundled default macOS entitlements grant only com.apple.security.cs.allow-jit (allow-unsigned-executable-memory and disable-library-validation are no longer granted). " +
|
|
35
|
+
"If you rely on the default (no mac.sign.entitlements and no build/entitlements.mac.plist) and your app loads native modules, frameworks, or plugins signed by another Team ID (or unsigned), " +
|
|
36
|
+
"add those entitlements back in build/entitlements.mac.plist. See https://www.electron.build/docs/migration/v27-breaking-changes#macos-default-entitlements-tightened";
|
|
37
|
+
// Advisory surfaced when a generic publish url carries a query string (typically a token). v27 electron-updater sends the feed
|
|
38
|
+
// query and credential headers only to downloads on the feed's origin.
|
|
39
|
+
export const FEED_QUERY_ADVISORY = "generic publish url with a query string detected. In v27, electron-updater sends the feed url's query string, and the credential headers from requestHeaders / addAuthHeader, " +
|
|
40
|
+
"only to downloads on the feed's origin (scheme, host and port); a download url in latest*.yml on another origin is requested without them. " +
|
|
41
|
+
"If those downloads need these credentials, serve the update files from the feed origin or use pre-signed URLs. " +
|
|
42
|
+
"See https://www.electron.build/docs/migration/v27-breaking-changes#update-credentials-stay-on-the-feeds-origin";
|
|
43
|
+
/**
|
|
44
|
+
* macOS signing/universal fields that v26 accepted as `null` (meaning "unset") but whose v27 type has no `null`
|
|
45
|
+
* branch, so a moved `null` would fail validation. They are dropped instead of moved. `identity: null` is not in
|
|
46
|
+
* this list — it means "skip signing" and `sign.identity` still accepts it.
|
|
47
|
+
*/
|
|
48
|
+
export const MAC_NULL_MEANS_UNSET_FIELDS = new Set(["type", "provisioningProfile", "binaries", "signIgnore", "singleArchFiles", "x64ArchFiles"]);
|
|
49
|
+
/** Platform keys that accepted a platform-level `asarUnpack` in v26. */
|
|
50
|
+
export const ASAR_PLATFORM_KEYS = ["mac", "mas", "masDev", "win", "linux"];
|
|
51
|
+
/** v26 `snap` options that the v27 `snapcraft.core24` shape does not accept (core24 uses the snapcraft CLI directly). */
|
|
52
|
+
export const SNAP_CORE24_UNSUPPORTED = ["allowNativeWayland", "useTemplateApp"];
|
|
53
|
+
/** A target name resolved like the build does: an ":<arch>" suffix is dropped and the name is lowercased ("RPM:x64" → "rpm"). */
|
|
54
|
+
export function normalizeTargetName(target) {
|
|
55
|
+
const suffixPos = target.lastIndexOf(":");
|
|
56
|
+
return (suffixPos > 0 ? target.substring(0, suffixPos) : target).toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
/** True when `target` (a string such as "deb" or "deb:arm64", a `{ target }` object, or an array of either) selects one of `names`. */
|
|
59
|
+
function hasTarget(target, names) {
|
|
60
|
+
if (target == null) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (typeof target === "string") {
|
|
64
|
+
return names.includes(normalizeTargetName(target));
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(target)) {
|
|
67
|
+
return target.some(it => hasTarget(it, names));
|
|
68
|
+
}
|
|
69
|
+
if (typeof target === "object") {
|
|
70
|
+
return hasTarget(target.target, names);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/** True when the build config selects one of `names` (via `<platform>.target` or the global target). */
|
|
75
|
+
function detectTarget(config, platform, names) {
|
|
76
|
+
return hasTarget(config[platform]?.target, names) || hasTarget(config.target, names);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* True when a custom `win.sign.sign` hook signs without `publisherName` and update signature verification is on (a certificate
|
|
80
|
+
* in the config doesn't matter: a hook always needs publisherName). Evaluated on the migrated config, so a v26
|
|
81
|
+
* `win.signtoolOptions.sign` counts too.
|
|
82
|
+
*/
|
|
83
|
+
function detectSignHookWithoutPublisherName(config) {
|
|
84
|
+
const win = config.win;
|
|
85
|
+
const sign = isPlainObject(win) ? win.sign : null;
|
|
86
|
+
// a function-valued hook cannot occur here: migrateConfig works on a JSON copy of the config
|
|
87
|
+
return isPlainObject(sign) && typeof sign.sign === "string" && sign.publisherName === undefined && win.verifyUpdateCodeSignature !== false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* True when a mac/mas/masDev section signs with electron-builder's defaults without naming its own entitlements
|
|
91
|
+
* file. A custom signer (string/function `sign`) or `sign.identity: null` (skip signing) bypasses the defaults.
|
|
92
|
+
*/
|
|
93
|
+
function detectDefaultMacEntitlements(config) {
|
|
94
|
+
return ["mac", "mas", "masDev"].some(p => {
|
|
95
|
+
const platform = config[p];
|
|
96
|
+
if (!isPlainObject(platform)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const sign = platform.sign;
|
|
100
|
+
if (typeof sign === "string" || typeof sign === "function") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return !(isPlainObject(sign) && (sign.entitlements != null || sign.identity === null));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** True when a root or platform/target-level `publish` (walked like step 7) has a generic provider whose url has a query string. */
|
|
107
|
+
function detectGenericFeedQuery(config) {
|
|
108
|
+
return [config, ...Object.values(config)].some(section => isPlainObject(section) &&
|
|
109
|
+
(Array.isArray(section.publish) ? section.publish : [section.publish]).some((entry) => isPlainObject(entry) && entry.provider === "generic" && typeof entry.url === "string" && entry.url.includes("?")));
|
|
110
|
+
}
|
|
111
|
+
function isPlainObject(value) {
|
|
112
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
113
|
+
}
|
|
9
114
|
/**
|
|
10
115
|
* Applies all v26→v27 config transformations to a parsed config object.
|
|
11
116
|
* Pure function — no I/O, safe to call in tests.
|
|
@@ -14,6 +119,7 @@ export function migrateConfig(raw) {
|
|
|
14
119
|
const c = JSON.parse(JSON.stringify(raw));
|
|
15
120
|
const changes = [];
|
|
16
121
|
const warnings = [];
|
|
122
|
+
const advisories = [];
|
|
17
123
|
// ── 1. electronCompile ────────────────────────────────────────────────────
|
|
18
124
|
if ("electronCompile" in c) {
|
|
19
125
|
delete c.electronCompile;
|
|
@@ -26,6 +132,32 @@ export function migrateConfig(raw) {
|
|
|
26
132
|
changes.push({ key, description: `removed ${key} (Electron is the only supported framework in v27)` });
|
|
27
133
|
}
|
|
28
134
|
}
|
|
135
|
+
// ── 2b. disableDefaultIgnoredFiles removed (root + platform configs) ──────
|
|
136
|
+
// mas/masDev are MacConfiguration-derived, so they accept the (now-removed) key too.
|
|
137
|
+
for (const obj of [c, c.mac, c.mas, c.masDev, c.win, c.linux]) {
|
|
138
|
+
if (obj != null && typeof obj === "object" && "disableDefaultIgnoredFiles" in obj) {
|
|
139
|
+
delete obj.disableDefaultIgnoredFiles;
|
|
140
|
+
changes.push({
|
|
141
|
+
key: "disableDefaultIgnoredFiles",
|
|
142
|
+
description: "removed disableDefaultIgnoredFiles (in v27, include a default-excluded file via an explicit `files` glob, e.g. `**/*.obj`)",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ── 2c. linux.syncDesktopName removed (always-on in v27) ──────────────────
|
|
147
|
+
// The flag is gone from the type; the behaviour it gated (deriving the installed .desktop filename
|
|
148
|
+
// from desktopName) is now always on. Leaving the key would fail v27 schema validation (noExtraProps).
|
|
149
|
+
if (c.linux != null && typeof c.linux === "object" && "syncDesktopName" in c.linux) {
|
|
150
|
+
const wasDisabled = c.linux.syncDesktopName === false;
|
|
151
|
+
delete c.linux.syncDesktopName;
|
|
152
|
+
changes.push({
|
|
153
|
+
key: "linux.syncDesktopName",
|
|
154
|
+
description: "removed linux.syncDesktopName (the installed .desktop filename is always synced from desktopName in v27)",
|
|
155
|
+
});
|
|
156
|
+
if (wasDisabled) {
|
|
157
|
+
warnings.push("linux.syncDesktopName: false disabled desktop-name syncing in v26. In v27 the installed .desktop filename is always derived from `desktopName` " +
|
|
158
|
+
"(falling back to executableName). If you relied on the old filename, set `desktopName` explicitly to control it.");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
29
161
|
// ── 3. npmSkipBuildFromSource → buildDependenciesFromSource ───────────────
|
|
30
162
|
if ("npmSkipBuildFromSource" in c) {
|
|
31
163
|
// will be picked up by the nativeModules step below
|
|
@@ -47,11 +179,20 @@ export function migrateConfig(raw) {
|
|
|
47
179
|
}
|
|
48
180
|
}
|
|
49
181
|
if ("nativeRebuilder" in c) {
|
|
50
|
-
|
|
182
|
+
if (c.nativeRebuilder === "legacy") {
|
|
183
|
+
// v27 dropped the app-builder binary rebuilder; rebuildMode only selects @electron/rebuild's mode.
|
|
184
|
+
warnings.push('nativeRebuilder: "legacy" (the app-builder binary rebuilder) was removed in v27 — native modules are always rebuilt with @electron/rebuild. ' +
|
|
185
|
+
'The value was dropped, so the default nativeModules.rebuildMode ("sequential") applies.');
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
sub.rebuildMode = c.nativeRebuilder;
|
|
189
|
+
}
|
|
51
190
|
delete c.nativeRebuilder;
|
|
52
191
|
changes.push({ key: "nativeRebuilder", description: "renamed nativeRebuilder → nativeModules.rebuildMode" });
|
|
53
192
|
}
|
|
54
|
-
c.nativeModules
|
|
193
|
+
if (Object.keys(sub).length > 0 || c.nativeModules != null) {
|
|
194
|
+
c.nativeModules = sub;
|
|
195
|
+
}
|
|
55
196
|
}
|
|
56
197
|
// ── 5. Legacy asar keys → asarUnpack ─────────────────────────────────────
|
|
57
198
|
// Hyphenated root-level variants
|
|
@@ -83,12 +224,12 @@ export function migrateConfig(raw) {
|
|
|
83
224
|
}
|
|
84
225
|
// ── 7. GithubOptions.vPrefixedTagName → tagNamePrefix ─────────────────────
|
|
85
226
|
if (c.publish != null) {
|
|
86
|
-
migratePublishEntries(c, "publish", changes);
|
|
227
|
+
migratePublishEntries(c, "publish", changes, warnings);
|
|
87
228
|
}
|
|
88
|
-
// Also handle publish nested inside mac
|
|
89
|
-
for (const
|
|
90
|
-
if (
|
|
91
|
-
migratePublishEntries(
|
|
229
|
+
// Also handle publish nested inside any platform/target section (mac, win, nsis, dmg, snap, …)
|
|
230
|
+
for (const value of Object.values(c)) {
|
|
231
|
+
if (isPlainObject(value) && value.publish != null) {
|
|
232
|
+
migratePublishEntries(value, "publish", changes, warnings);
|
|
92
233
|
}
|
|
93
234
|
}
|
|
94
235
|
// ── 8. snap → snapcraft restructuring ─────────────────────────────────────
|
|
@@ -114,10 +255,40 @@ export function migrateConfig(raw) {
|
|
|
114
255
|
delete sq.noMsi;
|
|
115
256
|
changes.push({ key: "squirrelWindows.noMsi", description: "replaced squirrelWindows.noMsi → squirrelWindows.msi (inverted boolean)" });
|
|
116
257
|
}
|
|
258
|
+
// ── 10b. squirrelWindows.customSquirrelVendorDir removed (not auto-migrated) ──
|
|
259
|
+
// The v27 replacement (a toolsets.squirrel ToolsetCustom bundle) needs a different directory layout, so the
|
|
260
|
+
// key is left in place: the build then fails with a targeted message instead of silently switching the
|
|
261
|
+
// vendored Squirrel binaries back to the default bundle.
|
|
262
|
+
if (isPlainObject(c.squirrelWindows) && "customSquirrelVendorDir" in c.squirrelWindows) {
|
|
263
|
+
warnings.push("squirrelWindows.customSquirrelVendorDir was removed in v27 and cannot be migrated automatically. " +
|
|
264
|
+
'Supply the custom bundle via toolsets.squirrel: { url: "file:///abs/path" } — the bundle must contain an electron-winstaller/vendor/ subtree ' +
|
|
265
|
+
"(customSquirrelVendorDir pointed at the vendor files directly) — or remove the key to use the default Squirrel bundle.");
|
|
266
|
+
}
|
|
117
267
|
// ── 11. asar consolidation ───────────────────────────────────────────────
|
|
118
268
|
// Move root-level asarUnpack / disableSanityCheckAsar / disableAsarIntegrity
|
|
119
|
-
// into the asar object.
|
|
120
|
-
|
|
269
|
+
// into the asar object. In v26 these applied to every platform independently of `asar`; in v27 they live
|
|
270
|
+
// inside it, and a platform-level `asar` replaces the root one wholesale — so capture them first for step 11b.
|
|
271
|
+
const rootAsarValue = c.asar;
|
|
272
|
+
const rootFlatAsar = {};
|
|
273
|
+
if ("asarUnpack" in c) {
|
|
274
|
+
rootFlatAsar.unpack = c.asarUnpack;
|
|
275
|
+
}
|
|
276
|
+
if ("disableSanityCheckAsar" in c) {
|
|
277
|
+
rootFlatAsar.disableSanityCheck = c.disableSanityCheckAsar;
|
|
278
|
+
}
|
|
279
|
+
if ("disableAsarIntegrity" in c) {
|
|
280
|
+
rootFlatAsar.disableIntegrity = c.disableAsarIntegrity;
|
|
281
|
+
}
|
|
282
|
+
if (c.asar === false) {
|
|
283
|
+
// No effect on the root build; step 11b still carries them into any platform that re-enables asar.
|
|
284
|
+
for (const key of ["asarUnpack", "disableSanityCheckAsar", "disableAsarIntegrity"]) {
|
|
285
|
+
if (key in c) {
|
|
286
|
+
delete c[key];
|
|
287
|
+
changes.push({ key, description: `removed ${key} (no effect with asar: false)` });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
121
292
|
const asarSub = typeof c.asar === "object" && c.asar != null ? { ...c.asar } : {};
|
|
122
293
|
if ("asarUnpack" in c) {
|
|
123
294
|
asarSub.unpack = mergeAsarUnpack(asarSub.unpack, c.asarUnpack);
|
|
@@ -137,35 +308,460 @@ export function migrateConfig(raw) {
|
|
|
137
308
|
if (c.asar === true) {
|
|
138
309
|
if (Object.keys(asarSub).length > 0) {
|
|
139
310
|
c.asar = asarSub;
|
|
311
|
+
changes.push({ key: "asar", description: `replaced asar: true with an asar object carrying ${Object.keys(asarSub).join(", ")}` });
|
|
140
312
|
}
|
|
141
313
|
else {
|
|
142
314
|
delete c.asar;
|
|
315
|
+
changes.push({ key: "asar", description: "removed redundant asar: true (asar is enabled by default; the explicit `true` is not needed)" });
|
|
143
316
|
}
|
|
144
|
-
changes.push({ key: "asar", description: "replaced asar: true with asar object (true is no longer a valid value)" });
|
|
145
317
|
}
|
|
146
318
|
else if (Object.keys(asarSub).length > 0) {
|
|
147
319
|
c.asar = asarSub;
|
|
148
320
|
}
|
|
149
321
|
}
|
|
150
|
-
// ──
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
322
|
+
// ── 11b. platform-level asar ──────────────────────────────────────────────
|
|
323
|
+
for (const platform of ASAR_PLATFORM_KEYS) {
|
|
324
|
+
if (isPlainObject(c[platform])) {
|
|
325
|
+
migratePlatformAsar(c, platform, rootAsarValue, rootFlatAsar, changes);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ── 12. win.sign unification ───────────────────────────────────────────────
|
|
329
|
+
// win.signtoolOptions → win.sign: { type: "signtool", … }
|
|
330
|
+
// win.azureSignOptions → win.sign: { type: "azure", … } (extra keys → additionalMetadata)
|
|
331
|
+
// win.signAndEditExecutable / win.signExecutable removed
|
|
332
|
+
if (c.win != null && typeof c.win === "object") {
|
|
333
|
+
const win = c.win;
|
|
334
|
+
// Remove defunct flags — signAndEditExecutable/signExecutable no longer exist in v27. Either one set to
|
|
335
|
+
// false skipped ALL signing in v26, so both map to win.sign: false (signAndEditExecutable also skipped
|
|
336
|
+
// resource editing, which always runs in v27 — that part has no equivalent).
|
|
337
|
+
const signingDisabled = win.signExecutable === false || win.signAndEditExecutable === false;
|
|
338
|
+
if ("signAndEditExecutable" in win) {
|
|
339
|
+
const val = win.signAndEditExecutable;
|
|
340
|
+
delete win.signAndEditExecutable;
|
|
341
|
+
if (val === false) {
|
|
342
|
+
warnings.push("win.signAndEditExecutable: false was used to skip both resource editing and signing. " +
|
|
343
|
+
"In v27, resource editing always runs; signing is still skipped via win.sign: false. " +
|
|
344
|
+
"There is no v27 equivalent that also skips resource editing — apply resources manually if needed.");
|
|
345
|
+
}
|
|
346
|
+
changes.push({ key: "win.signAndEditExecutable", description: "removed win.signAndEditExecutable (resource editing always runs in v27)" });
|
|
347
|
+
}
|
|
348
|
+
if ("signExecutable" in win) {
|
|
349
|
+
delete win.signExecutable;
|
|
350
|
+
changes.push({ key: "win.signExecutable", description: "removed win.signExecutable (signing is enabled by default when credentials are available)" });
|
|
351
|
+
}
|
|
352
|
+
if (signingDisabled && win.sign == null) {
|
|
353
|
+
win.sign = false;
|
|
354
|
+
changes.push({ key: "win.sign", description: "set win.sign: false (v26 signExecutable/signAndEditExecutable: false disabled signing; resource editing still runs)" });
|
|
355
|
+
}
|
|
356
|
+
if (win.sign === false && (win.azureSignOptions != null || win.signtoolOptions != null)) {
|
|
357
|
+
// Signing was switched off, so the legacy options never applied. Only their key names are reported —
|
|
358
|
+
// the values can hold certificate passwords.
|
|
359
|
+
const dropped = ["signtoolOptions", "azureSignOptions"].filter(k => win[k] != null);
|
|
360
|
+
for (const k of dropped) {
|
|
361
|
+
delete win[k];
|
|
362
|
+
changes.push({ key: `win.${k}`, description: `removed win.${k} (signing is disabled with win.sign: false)` });
|
|
363
|
+
}
|
|
364
|
+
warnings.push(`win.sign is false (signing disabled), so [${dropped.map(k => `win.${k}`).join(", ")}] had no effect and ${dropped.length === 1 ? "was" : "were"} removed. ` +
|
|
365
|
+
'To sign in v27, replace win.sign: false with win.sign: { type: "signtool" | "azure", … } carrying those options.');
|
|
366
|
+
}
|
|
367
|
+
const hasAzure = win.azureSignOptions != null;
|
|
368
|
+
const hasSigntool = win.signtoolOptions != null;
|
|
369
|
+
if (hasAzure || hasSigntool) {
|
|
370
|
+
const signAlreadySet = "sign" in win && win.sign !== null && win.sign !== undefined;
|
|
371
|
+
if (signAlreadySet) {
|
|
372
|
+
warnings.push("win.sign is already set alongside " +
|
|
373
|
+
(hasAzure ? "win.azureSignOptions" : "win.signtoolOptions") +
|
|
374
|
+
". Remove the legacy key manually after verifying win.sign is correct.");
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
if (hasAzure && hasSigntool) {
|
|
378
|
+
warnings.push("Both win.azureSignOptions and win.signtoolOptions are set. " +
|
|
379
|
+
"win.signtoolOptions will be dropped and win.azureSignOptions will be migrated to win.sign: { type: 'azure', … } (Azure took priority in v26). " +
|
|
380
|
+
"Verify the migrated win.sign block is correct for your project.");
|
|
381
|
+
delete win.signtoolOptions;
|
|
382
|
+
}
|
|
383
|
+
if (hasAzure) {
|
|
384
|
+
const azure = { ...win.azureSignOptions };
|
|
385
|
+
delete win.azureSignOptions;
|
|
386
|
+
// Move extra (untyped) keys into additionalMetadata
|
|
387
|
+
const extra = {};
|
|
388
|
+
for (const [k, v] of Object.entries(azure)) {
|
|
389
|
+
if (!AZURE_KNOWN_FIELDS.has(k) && typeof v === "string") {
|
|
390
|
+
extra[k] = v;
|
|
391
|
+
delete azure[k];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (Object.keys(extra).length > 0) {
|
|
395
|
+
azure.additionalMetadata = { ...(azure.additionalMetadata ?? {}), ...extra };
|
|
396
|
+
changes.push({
|
|
397
|
+
key: "win.azureSignOptions",
|
|
398
|
+
description: `moved extra keys [${Object.keys(extra).join(", ")}] into win.sign.additionalMetadata`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
win.sign = { ...azure, type: "azure" };
|
|
402
|
+
changes.push({ key: "win.azureSignOptions", description: 'moved win.azureSignOptions → win.sign: { type: "azure", … }' });
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const signtool = { ...win.signtoolOptions };
|
|
406
|
+
delete win.signtoolOptions;
|
|
407
|
+
win.sign = { ...signtool, type: "signtool" };
|
|
408
|
+
changes.push({ key: "win.signtoolOptions", description: 'moved win.signtoolOptions → win.sign: { type: "signtool", … }' });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ── 13. mac/mas/masDev signing + universal consolidation ──────────────────
|
|
414
|
+
// Signing fields (incl. identity) move into `sign`; mergeASARs/singleArchFiles/x64ArchFiles into `universal`.
|
|
415
|
+
for (const platform of ["mac", "mas", "masDev"]) {
|
|
416
|
+
if (c[platform] != null && typeof c[platform] === "object") {
|
|
417
|
+
migrateMacSigning(c[platform], platform, changes, warnings);
|
|
418
|
+
migrateMacUniversal(c[platform], platform, changes);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ── 14. electronDownload → electronGet ────────────────────────────────────
|
|
422
|
+
if ("electronDownload" in c) {
|
|
423
|
+
migrateElectronDownload(c, "electronDownload", changes, warnings);
|
|
424
|
+
}
|
|
425
|
+
else if (isPlainObject(c.electronGet) && Object.keys(c.electronGet).some(isLegacyElectronDownloadKey)) {
|
|
426
|
+
// A hand-renamed `electronGet` that still carries the v26 shape.
|
|
427
|
+
migrateElectronDownload(c, "electronGet", changes, warnings);
|
|
428
|
+
}
|
|
429
|
+
// ── 15. toolsets ──────────────────────────────────────────────────────────
|
|
430
|
+
if (isPlainObject(c.toolsets)) {
|
|
431
|
+
migrateToolsets(c.toolsets, changes);
|
|
432
|
+
}
|
|
433
|
+
// Advisories (not config changes): v27 runtime defaults and behavior changes that a config rewrite cannot address.
|
|
434
|
+
if (detectTarget(c, "win", ["nsis-web"])) {
|
|
435
|
+
advisories.push(NSIS_WEB_ADVISORY);
|
|
436
|
+
}
|
|
437
|
+
if (detectDefaultMacEntitlements(c)) {
|
|
438
|
+
advisories.push(MAC_ENTITLEMENTS_ADVISORY);
|
|
439
|
+
}
|
|
440
|
+
if (c.nsis?.perMachine === true || c.nsisWeb?.perMachine === true) {
|
|
441
|
+
advisories.push(NSIS_PER_MACHINE_UPDATE_ADVISORY);
|
|
442
|
+
}
|
|
443
|
+
if (detectSignHookWithoutPublisherName(c)) {
|
|
444
|
+
advisories.push(WIN_SIGN_HOOK_PUBLISHER_NAME_ADVISORY);
|
|
445
|
+
}
|
|
446
|
+
if (detectGenericFeedQuery(c)) {
|
|
447
|
+
advisories.push(FEED_QUERY_ADVISORY);
|
|
448
|
+
}
|
|
449
|
+
// ── nsis/nsisWeb/portable customNsisBinary / customNsisResources removed ──
|
|
450
|
+
// Kept keys are reported only after `modified` is computed: a key that is merely kept must not re-serialize the file
|
|
451
|
+
// (dropping YAML/JSON5 comments) when nothing else changed. The build then fails with the same targeted message.
|
|
452
|
+
const keptKeyWarnings = [];
|
|
453
|
+
migrateCustomNsis(c, changes, warnings, keptKeyWarnings);
|
|
454
|
+
const modified = changes.length > 0 || warnings.length > 0;
|
|
455
|
+
warnings.push(...keptKeyWarnings);
|
|
456
|
+
return { migrated: c, changes, warnings, advisories, modified };
|
|
457
|
+
}
|
|
458
|
+
/** The build-time guard's message for a removed key (`nsis.customNsisBinary`), reused for the keys migrate-schema leaves in place. */
|
|
459
|
+
export function legacyKeyMessage(fullPath) {
|
|
460
|
+
const option = RESOLVED_LEGACY_CONFIG_OPTIONS.find(o => o.fullPath === fullPath);
|
|
461
|
+
if (option == null) {
|
|
462
|
+
throw new Error(`no legacy config option for ${fullPath}`);
|
|
463
|
+
}
|
|
464
|
+
return formatLegacyOptionMessage(option);
|
|
465
|
+
}
|
|
466
|
+
/** Warning for a `portable.customNsisBinary.debugLogging` that migrate-schema drops. */
|
|
467
|
+
export const PORTABLE_DEBUG_LOGGING_DROPPED = "portable.customNsisBinary.debugLogging was dropped: it never had an effect on portable targets (the portable template does not enable NSIS logging), and v27 has no portable equivalent.";
|
|
468
|
+
/**
|
|
469
|
+
* `customNsisBinary.debugLogging` moves to `<section>.installerDebugLogging` (nsis / nsisWeb; dropped for portable), and an
|
|
470
|
+
* emptied or null `customNsisBinary` / `customNsisResources` is removed. A custom bundle (url / checksum / version, or the resources
|
|
471
|
+
* bundle) is never converted to `toolsets.nsis`: the bundle layout differs (it must carry the NSIS plugins), so the key is kept and reported.
|
|
472
|
+
*/
|
|
473
|
+
function migrateCustomNsis(c, changes, warnings, keptKeyWarnings) {
|
|
474
|
+
for (const section of NSIS_CONFIG_KEYS) {
|
|
475
|
+
const options = c[section];
|
|
476
|
+
if (!isPlainObject(options)) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const binaryPath = `${section}.customNsisBinary`;
|
|
480
|
+
const binary = options.customNsisBinary;
|
|
481
|
+
if (binary === null) {
|
|
482
|
+
delete options.customNsisBinary;
|
|
483
|
+
changes.push({ key: binaryPath, description: `removed ${binaryPath}: null (the key was removed in v27)` });
|
|
484
|
+
}
|
|
485
|
+
else if (isPlainObject(binary)) {
|
|
486
|
+
if ("debugLogging" in binary) {
|
|
487
|
+
const debugLogging = binary.debugLogging;
|
|
488
|
+
delete binary.debugLogging;
|
|
489
|
+
if (section === "portable") {
|
|
490
|
+
if (debugLogging != null) {
|
|
491
|
+
warnings.push(PORTABLE_DEBUG_LOGGING_DROPPED);
|
|
492
|
+
}
|
|
493
|
+
changes.push({ key: `${binaryPath}.debugLogging`, description: `removed ${binaryPath}.debugLogging (no effect on portable targets)` });
|
|
494
|
+
}
|
|
495
|
+
else if (debugLogging != null && !("installerDebugLogging" in options)) {
|
|
496
|
+
options.installerDebugLogging = debugLogging;
|
|
497
|
+
changes.push({ key: `${binaryPath}.debugLogging`, description: `moved ${binaryPath}.debugLogging → ${section}.installerDebugLogging` });
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
changes.push({ key: `${binaryPath}.debugLogging`, description: `removed ${binaryPath}.debugLogging` });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (Object.values(binary).every(v => v == null)) {
|
|
504
|
+
delete options.customNsisBinary;
|
|
505
|
+
changes.push({ key: binaryPath, description: `removed ${binaryPath} (no custom bundle was set)` });
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
keptKeyWarnings.push(legacyKeyMessage(binaryPath));
|
|
158
509
|
}
|
|
159
510
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
511
|
+
else if (binary !== undefined) {
|
|
512
|
+
keptKeyWarnings.push(legacyKeyMessage(binaryPath));
|
|
513
|
+
}
|
|
514
|
+
const resourcesPath = `${section}.customNsisResources`;
|
|
515
|
+
if (options.customNsisResources === null) {
|
|
516
|
+
delete options.customNsisResources;
|
|
517
|
+
changes.push({ key: resourcesPath, description: `removed ${resourcesPath}: null (the key was removed in v27)` });
|
|
518
|
+
}
|
|
519
|
+
else if (options.customNsisResources !== undefined) {
|
|
520
|
+
keptKeyWarnings.push(legacyKeyMessage(resourcesPath));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Moves macOS signing fields from a platform config (mac/mas/masDev) into the `sign` bag and
|
|
526
|
+
* renames `signIgnore` → `sign.ignore`. In v26 `sign` was only a custom-signer (string path / module id),
|
|
527
|
+
* so a non-object `sign` cannot absorb these options — that case is warned about, not clobbered.
|
|
528
|
+
* A bare `sign: null` (v26 "no custom signer") is dropped so it does not flip to v27's "skip signing".
|
|
529
|
+
*/
|
|
530
|
+
function migrateMacSigning(platform, name, changes, warnings) {
|
|
531
|
+
const present = MAC_SIGN_FIELDS.filter(f => f in platform);
|
|
532
|
+
const hasSignIgnore = "signIgnore" in platform;
|
|
533
|
+
const existingSign = platform.sign;
|
|
534
|
+
const signIsCustom = typeof existingSign === "string" || typeof existingSign === "function";
|
|
535
|
+
// Removed outright, not moved: ElectronSignOptions omits these and the schema rejects them, so
|
|
536
|
+
// relocating them under `sign` would emit a config that fails validation on the next build.
|
|
537
|
+
for (const field of MAC_SIGN_REMOVED_FIELDS) {
|
|
538
|
+
if (field in platform) {
|
|
539
|
+
delete platform[field];
|
|
540
|
+
changes.push({ key: `${name}.${field}`, description: `removed ${name}.${field} (@electron/osx-sign 2.x dropped the spctl --assess step; there is no ${name}.sign.${field})` });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (present.length === 0 && !hasSignIgnore) {
|
|
544
|
+
// Only thing to fix is a semantically-flipped bare null
|
|
545
|
+
if (existingSign === null) {
|
|
546
|
+
delete platform.sign;
|
|
547
|
+
changes.push({ key: `${name}.sign`, description: `removed ${name}.sign: null (v26 "no custom signer" = v27 default; sign: null now means skip signing)` });
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (signIsCustom) {
|
|
552
|
+
const fields = [...present, ...(hasSignIgnore ? ["signIgnore"] : [])].join(", ");
|
|
553
|
+
warnings.push(`${name}.sign is a custom signing function/path, which cannot hold options. Move [${fields}] into an ElectronSignOptions object manually, or keep the custom signer and drop them.`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// existingSign is null/undefined or already an object → build the merged options bag
|
|
557
|
+
const signObj = existingSign != null && typeof existingSign === "object" ? { ...existingSign } : {};
|
|
558
|
+
for (const f of present) {
|
|
559
|
+
if (dropNullField(platform, f, name, changes)) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
signObj[f] = platform[f];
|
|
563
|
+
delete platform[f];
|
|
564
|
+
changes.push({ key: `${name}.${f}`, description: `moved ${name}.${f} → ${name}.sign.${f}` });
|
|
565
|
+
}
|
|
566
|
+
if (hasSignIgnore && !dropNullField(platform, "signIgnore", name, changes)) {
|
|
567
|
+
signObj.ignore = platform.signIgnore;
|
|
568
|
+
delete platform.signIgnore;
|
|
569
|
+
changes.push({ key: `${name}.signIgnore`, description: `renamed ${name}.signIgnore → ${name}.sign.ignore` });
|
|
570
|
+
}
|
|
571
|
+
if (Object.keys(signObj).length > 0 || existingSign != null) {
|
|
572
|
+
platform.sign = signObj;
|
|
573
|
+
}
|
|
574
|
+
else if (existingSign === null) {
|
|
575
|
+
// Every moved field was a dropped null — a bare `sign: null` left behind would now mean "skip signing".
|
|
576
|
+
delete platform.sign;
|
|
577
|
+
changes.push({ key: `${name}.sign`, description: `removed ${name}.sign: null (v26 "no custom signer" = v27 default; sign: null now means skip signing)` });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** Drops a v26 `null` ("unset") for a field whose v27 type has no null branch. Returns true when dropped. */
|
|
581
|
+
function dropNullField(platform, field, name, changes) {
|
|
582
|
+
if (platform[field] !== null || !MAC_NULL_MEANS_UNSET_FIELDS.has(field)) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
delete platform[field];
|
|
586
|
+
changes.push({ key: `${name}.${field}`, description: `removed ${name}.${field}: null (null meant "unset" in v26; the v27 option does not accept null)` });
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
/** Moves universal-build fields from a platform config into the `universal` bag. */
|
|
590
|
+
function migrateMacUniversal(platform, name, changes) {
|
|
591
|
+
const present = MAC_UNIVERSAL_FIELDS.filter(f => f in platform);
|
|
592
|
+
if (present.length === 0) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const universalObj = platform.universal != null && typeof platform.universal === "object" ? { ...platform.universal } : {};
|
|
596
|
+
for (const f of present) {
|
|
597
|
+
if (dropNullField(platform, f, name, changes)) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
universalObj[f] = platform[f];
|
|
601
|
+
delete platform[f];
|
|
602
|
+
changes.push({ key: `${name}.${f}`, description: `moved ${name}.${f} → ${name}.universal.${f}` });
|
|
603
|
+
}
|
|
604
|
+
if (Object.keys(universalObj).length > 0 || platform.universal != null) {
|
|
605
|
+
platform.universal = universalObj;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Moves a platform-level `asarUnpack` into that platform's `asar` object.
|
|
610
|
+
*
|
|
611
|
+
* v26 concatenated root and platform `asarUnpack` and always applied the root `disableSanityCheckAsar` /
|
|
612
|
+
* `disableAsarIntegrity`, whatever the platform's `asar` was. v27 keeps all three inside `asar`, and a
|
|
613
|
+
* platform-level `asar` replaces the root one entirely — so a platform that overrides `asar` (or gains one
|
|
614
|
+
* here) gets the root values folded in to keep the v26 result.
|
|
615
|
+
*/
|
|
616
|
+
function migratePlatformAsar(c, name, rootAsarValue, rootFlatAsar, changes) {
|
|
617
|
+
const platform = c[name];
|
|
618
|
+
const hasUnpack = "asarUnpack" in platform;
|
|
619
|
+
const own = platform.asar;
|
|
620
|
+
if (own == null) {
|
|
621
|
+
// Inherits the root asar — only needs an override of its own to carry a platform asarUnpack.
|
|
622
|
+
if (!hasUnpack) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (rootAsarValue === false) {
|
|
626
|
+
delete platform.asarUnpack;
|
|
627
|
+
changes.push({ key: `${name}.asarUnpack`, description: `removed ${name}.asarUnpack (no effect with asar: false)` });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const next = isPlainObject(c.asar) ? { ...c.asar } : {};
|
|
631
|
+
next.unpack = mergeAsarUnpack(next.unpack, platform.asarUnpack);
|
|
632
|
+
platform.asar = next;
|
|
633
|
+
delete platform.asarUnpack;
|
|
634
|
+
changes.push({
|
|
635
|
+
key: `${name}.asarUnpack`,
|
|
636
|
+
description: `moved ${name}.asarUnpack → ${name}.asar.unpack (merged with the root asar options, since a platform-level asar replaces the root one)`,
|
|
637
|
+
});
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (own === false) {
|
|
641
|
+
if (hasUnpack) {
|
|
642
|
+
delete platform.asarUnpack;
|
|
643
|
+
changes.push({ key: `${name}.asarUnpack`, description: `removed ${name}.asarUnpack (no effect with ${name}.asar: false)` });
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// own is true or an AsarOptions object: fold the v26 root-level options (and the platform asarUnpack) into it.
|
|
648
|
+
const next = isPlainObject(own) ? { ...own } : {};
|
|
649
|
+
const folded = [];
|
|
650
|
+
if (rootFlatAsar.unpack != null || hasUnpack) {
|
|
651
|
+
// v26 order: root patterns first, then the platform's own.
|
|
652
|
+
let unpack = undefined;
|
|
653
|
+
for (const it of [rootFlatAsar.unpack, next.unpack, hasUnpack ? platform.asarUnpack : undefined]) {
|
|
654
|
+
if (it != null) {
|
|
655
|
+
unpack = mergeAsarUnpack(unpack, it);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (unpack !== undefined) {
|
|
659
|
+
next.unpack = unpack;
|
|
660
|
+
folded.push("unpack");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
for (const key of ["disableSanityCheck", "disableIntegrity"]) {
|
|
664
|
+
if (key in rootFlatAsar && !(key in next)) {
|
|
665
|
+
next[key] = rootFlatAsar[key];
|
|
666
|
+
folded.push(key);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (hasUnpack) {
|
|
670
|
+
delete platform.asarUnpack;
|
|
671
|
+
}
|
|
672
|
+
if (folded.length === 0) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
platform.asar = next;
|
|
676
|
+
changes.push({
|
|
677
|
+
key: hasUnpack ? `${name}.asarUnpack` : `${name}.asar`,
|
|
678
|
+
description: `folded ${folded.map(k => `asar.${k}`).join(", ")} into ${name}.asar (a platform-level asar replaces the root one in v27, so the v26 root-level values are copied)`,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
/** Removes `null` toolset values (rejected by the v27 schema) and remaps pins to bundles that are no longer published. */
|
|
682
|
+
function migrateToolsets(toolsets, changes) {
|
|
683
|
+
for (const [key, value] of Object.entries(toolsets)) {
|
|
684
|
+
if (value === null) {
|
|
685
|
+
delete toolsets[key];
|
|
162
686
|
changes.push({
|
|
163
|
-
key:
|
|
164
|
-
description: `
|
|
687
|
+
key: `toolsets.${key}`,
|
|
688
|
+
description: `removed toolsets.${key}: null (v27 rejects null; unset resolves to "latest", the newest bundle — pin "0.0.0" to keep the legacy bundle v26 used)`,
|
|
165
689
|
});
|
|
166
690
|
}
|
|
167
691
|
}
|
|
168
|
-
|
|
692
|
+
if (toolsets.appimage === "1.0.2") {
|
|
693
|
+
toolsets.appimage = "1.0.3";
|
|
694
|
+
changes.push({
|
|
695
|
+
key: "toolsets.appimage",
|
|
696
|
+
description: 'replaced toolsets.appimage: "1.0.2" with "1.0.3" (1.0.2 is no longer offered; 1.0.3 is the same runtime with the #9598 fix)',
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Renames `electronDownload` → `electronGet` and reshapes it to ElectronGetOptions.
|
|
702
|
+
* `mirror` → `mirrorOptions.mirror`, `isVerifyChecksum: false` → `unsafelyDisableChecksums: true`.
|
|
703
|
+
* Fields with no v5 equivalent are dropped with a warning.
|
|
704
|
+
*/
|
|
705
|
+
function migrateElectronDownload(c, sourceKey, changes, warnings) {
|
|
706
|
+
const old = c[sourceKey];
|
|
707
|
+
delete c[sourceKey];
|
|
708
|
+
if (old == null || typeof old !== "object") {
|
|
709
|
+
changes.push({ key: "electronDownload", description: "renamed electronDownload → electronGet" });
|
|
710
|
+
if (old != null && c.electronGet == null) {
|
|
711
|
+
c.electronGet = old;
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const next = {};
|
|
716
|
+
if ("mirror" in old && old.mirror != null) {
|
|
717
|
+
next.mirrorOptions = { ...(next.mirrorOptions ?? {}), mirror: old.mirror };
|
|
718
|
+
}
|
|
719
|
+
if (old.isVerifyChecksum === false) {
|
|
720
|
+
next.unsafelyDisableChecksums = true;
|
|
721
|
+
}
|
|
722
|
+
// Carry over any already-valid ElectronGetOptions fields (mirrorOptions, unsafelyDisableChecksums, force, etc.)
|
|
723
|
+
for (const [k, v] of Object.entries(old)) {
|
|
724
|
+
if (k !== "mirror" && k !== "isVerifyChecksum" && !ELECTRON_DOWNLOAD_DROPPED.includes(k)) {
|
|
725
|
+
next[k] = v;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const dropped = ELECTRON_DOWNLOAD_DROPPED.filter(k => k in old);
|
|
729
|
+
if (dropped.length > 0) {
|
|
730
|
+
warnings.push(`electronGet (formerly electronDownload) dropped [${dropped.join(", ")}] — these have no equivalent in @electron/get v5. Set a mirror via electronGet.mirrorOptions if needed.`);
|
|
731
|
+
}
|
|
732
|
+
// An electronGet that already exists is the v27 intent: fold the legacy values in underneath it, never over it.
|
|
733
|
+
const existing = sourceKey === "electronDownload" && isPlainObject(c.electronGet) ? c.electronGet : null;
|
|
734
|
+
if (existing != null) {
|
|
735
|
+
const conflicts = [];
|
|
736
|
+
let mergedMirrorOptions;
|
|
737
|
+
for (const [k, v] of Object.entries(next)) {
|
|
738
|
+
if (k === "mirrorOptions" && isPlainObject(v) && isPlainObject(existing.mirrorOptions)) {
|
|
739
|
+
const own = existing.mirrorOptions;
|
|
740
|
+
conflicts.push(...Object.keys(v)
|
|
741
|
+
.filter(m => m in own && JSON.stringify(own[m]) !== JSON.stringify(v[m]))
|
|
742
|
+
.map(m => `mirrorOptions.${m}`));
|
|
743
|
+
mergedMirrorOptions = { ...v, ...own };
|
|
744
|
+
}
|
|
745
|
+
else if (k in existing && JSON.stringify(existing[k]) !== JSON.stringify(v)) {
|
|
746
|
+
conflicts.push(k);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
Object.assign(next, existing, mergedMirrorOptions != null ? { mirrorOptions: mergedMirrorOptions } : {});
|
|
750
|
+
if (conflicts.length > 0) {
|
|
751
|
+
warnings.push(`Both electronDownload and electronGet set [${conflicts.join(", ")}]; kept the existing electronGet values and dropped the electronDownload ones. Verify the merged electronGet.`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
c.electronGet = next;
|
|
755
|
+
changes.push({
|
|
756
|
+
key: sourceKey,
|
|
757
|
+
description: sourceKey === "electronDownload"
|
|
758
|
+
? "renamed electronDownload → electronGet (mirror → mirrorOptions.mirror; isVerifyChecksum → unsafelyDisableChecksums)"
|
|
759
|
+
: "reshaped electronGet to the v27 options (mirror → mirrorOptions.mirror; isVerifyChecksum → unsafelyDisableChecksums)",
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
/** Keys of the v26 `electronDownload` shape that are not valid v27 `electronGet` keys. */
|
|
763
|
+
export function isLegacyElectronDownloadKey(key) {
|
|
764
|
+
return key === "mirror" || key === "isVerifyChecksum" || ELECTRON_DOWNLOAD_DROPPED.includes(key);
|
|
169
765
|
}
|
|
170
766
|
function mergeAsarUnpack(existing, incoming) {
|
|
171
767
|
if (existing == null) {
|
|
@@ -174,27 +770,36 @@ function mergeAsarUnpack(existing, incoming) {
|
|
|
174
770
|
const arr = (Array.isArray(existing) ? existing : [existing]).concat(Array.isArray(incoming) ? incoming : [incoming]);
|
|
175
771
|
return arr.length === 1 ? arr[0] : arr;
|
|
176
772
|
}
|
|
177
|
-
function migratePublishEntries(parent, key, changes) {
|
|
773
|
+
function migratePublishEntries(parent, key, changes, warnings) {
|
|
178
774
|
const entries = Array.isArray(parent[key]) ? parent[key] : [parent[key]];
|
|
179
775
|
let changed = false;
|
|
180
776
|
for (const entry of entries) {
|
|
181
|
-
if (entry
|
|
182
|
-
|
|
777
|
+
if (entry == null || entry.provider !== "github") {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if ("vPrefixedTagName" in entry) {
|
|
781
|
+
// v26 githubTagPrefix(): a non-empty tagNamePrefix won; otherwise vPrefixedTagName (default true) chose "v" or "".
|
|
782
|
+
entry.tagNamePrefix = entry.tagNamePrefix ? entry.tagNamePrefix : entry.vPrefixedTagName === false ? "" : "v";
|
|
183
783
|
delete entry.vPrefixedTagName;
|
|
184
784
|
changed = true;
|
|
185
785
|
}
|
|
186
|
-
if (entry
|
|
187
|
-
|
|
786
|
+
else if (entry.tagNamePrefix === "") {
|
|
787
|
+
// v26 ignored an empty tagNamePrefix and still tagged "v1.2.3"; v27 honors it and would tag "1.2.3".
|
|
788
|
+
entry.tagNamePrefix = "v";
|
|
188
789
|
changed = true;
|
|
790
|
+
warnings.push('A GitHub publish entry set tagNamePrefix: "". v26 ignored an empty prefix and tagged releases "v<version>"; v27 honors it. ' +
|
|
791
|
+
'It was rewritten to tagNamePrefix: "v" to keep your existing tag names — set it back to "" if you want unprefixed tags from now on.');
|
|
189
792
|
}
|
|
793
|
+
// GitLab keeps vPrefixedTagName in v27 — it remains in the type, scheme, and runtime
|
|
794
|
+
// (gitlabPublisher honors it) and has no tagNamePrefix equivalent, so leave it untouched.
|
|
190
795
|
}
|
|
191
796
|
if (changed) {
|
|
192
|
-
changes.push({ key: `${key}[].vPrefixedTagName`, description: "replaced vPrefixedTagName with tagNamePrefix on GitHub publish entries
|
|
797
|
+
changes.push({ key: `${key}[].vPrefixedTagName`, description: "replaced vPrefixedTagName with tagNamePrefix on GitHub publish entries" });
|
|
193
798
|
}
|
|
194
799
|
}
|
|
195
800
|
// snap bases recognized by the v27 snapcraft config. "custom" uses an inline/path yaml
|
|
196
801
|
// and must be moved verbatim rather than nested under a per-base sub-key.
|
|
197
|
-
const SNAP_BASES = new Set(["core18", "core20", "core22", "core24", "custom"]);
|
|
802
|
+
export const SNAP_BASES = new Set(["core18", "core20", "core22", "core24", "custom"]);
|
|
198
803
|
/**
|
|
199
804
|
* Migrates the removed flat `snap` config into the v27 `snapcraft` shape:
|
|
200
805
|
* { snapcraft: { base, [base]: { ...rest } } }
|
|
@@ -227,6 +832,15 @@ function migrateSnap(c, changes, warnings) {
|
|
|
227
832
|
base = "core20";
|
|
228
833
|
assumed = true;
|
|
229
834
|
}
|
|
835
|
+
if (base === "core24") {
|
|
836
|
+
const unsupported = SNAP_CORE24_UNSUPPORTED.filter(k => k in rest);
|
|
837
|
+
for (const k of unsupported) {
|
|
838
|
+
delete rest[k];
|
|
839
|
+
}
|
|
840
|
+
if (unsupported.length > 0) {
|
|
841
|
+
warnings.push(`snap config set [${unsupported.join(", ")}], which the v27 snapcraft.core24 options do not support (core24 builds with the snapcraft CLI directly); they were dropped.`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
230
844
|
snapcraft.base = base;
|
|
231
845
|
snapcraft[base] = { ...(snapcraft[base] ?? {}), ...rest };
|
|
232
846
|
c.snapcraft = snapcraft;
|
|
@@ -261,6 +875,8 @@ function serializeConfig(content, format, originalText) {
|
|
|
261
875
|
const indent = indentMatch ? indentMatch[1].length : 2;
|
|
262
876
|
return JSON.stringify(content, null, indent) + "\n";
|
|
263
877
|
}
|
|
878
|
+
/** Extensions of the `electron-builder.<ext>` files auto-detected when `--config` is omitted, in lookup order. */
|
|
879
|
+
const CONFIG_FILE_EXTENSIONS = ["yml", "yaml", "json", "json5", "toml", "js", "cjs", "mjs", "ts"];
|
|
264
880
|
async function findAndLoadConfig(projectDir, explicitConfigPath) {
|
|
265
881
|
if (explicitConfigPath != null) {
|
|
266
882
|
const abs = path.resolve(projectDir, explicitConfigPath);
|
|
@@ -288,7 +904,7 @@ async function findAndLoadConfig(projectDir, explicitConfigPath) {
|
|
|
288
904
|
}
|
|
289
905
|
}
|
|
290
906
|
// Standalone config files
|
|
291
|
-
const candidates =
|
|
907
|
+
const candidates = CONFIG_FILE_EXTENSIONS.map(ext => path.join(projectDir, `electron-builder.${ext}`));
|
|
292
908
|
for (const candidate of candidates) {
|
|
293
909
|
const text = await readFileSafe(candidate);
|
|
294
910
|
if (text != null) {
|
|
@@ -299,6 +915,10 @@ async function findAndLoadConfig(projectDir, explicitConfigPath) {
|
|
|
299
915
|
return null;
|
|
300
916
|
}
|
|
301
917
|
function parseConfig(text, format) {
|
|
918
|
+
if (format === "js") {
|
|
919
|
+
// Programmatic configs (JS/TS/MJS/CJS) can't be parsed; migrateSchema will bail with manual steps
|
|
920
|
+
return {};
|
|
921
|
+
}
|
|
302
922
|
if (format === "yaml") {
|
|
303
923
|
const yaml = _require("js-yaml");
|
|
304
924
|
return yaml.load(text);
|
|
@@ -314,7 +934,7 @@ function parseConfig(text, format) {
|
|
|
314
934
|
return JSON.parse(text);
|
|
315
935
|
}
|
|
316
936
|
async function readFileSafe(p) {
|
|
317
|
-
return fs.readFile(p, "utf8")
|
|
937
|
+
return orNullIfFileNotExist(fs.readFile(p, "utf8"));
|
|
318
938
|
}
|
|
319
939
|
async function writeBackConfig(found, migrated, projectDir) {
|
|
320
940
|
if (found.isPackageJson) {
|
|
@@ -359,36 +979,46 @@ export async function migrateSchema(args) {
|
|
|
359
979
|
const dryRun = args["dry-run"] ?? false;
|
|
360
980
|
const found = await findAndLoadConfig(projectDir, configPath);
|
|
361
981
|
if (found == null) {
|
|
362
|
-
|
|
982
|
+
// A missing --config file was already reported by findAndLoadConfig.
|
|
983
|
+
if (configPath == null) {
|
|
984
|
+
log.error({ projectDir }, `no config found — checked the "build" key of package.json and electron-builder.{${CONFIG_FILE_EXTENSIONS.join(",")}}. ` +
|
|
985
|
+
"If your config file has another name (e.g. one you pass to `electron-builder --config`), run `electron-builder migrate-schema --config <path>`");
|
|
986
|
+
}
|
|
363
987
|
process.exit(1);
|
|
364
988
|
}
|
|
365
989
|
const location = found.isPackageJson ? 'package.json ("build" key)' : path.relative(projectDir, found.configFile);
|
|
366
990
|
log.info({ file: location }, "loaded config");
|
|
367
|
-
// Programmatic formats
|
|
991
|
+
// Programmatic formats (JS/TS) are rewritten surgically via an AST-located text codemod.
|
|
368
992
|
if (found.format === "js") {
|
|
369
|
-
|
|
370
|
-
printManualSteps();
|
|
993
|
+
await migrateProgrammaticConfigFile(found, location, dryRun);
|
|
371
994
|
return;
|
|
372
995
|
}
|
|
373
996
|
// TOML: can parse but not serialize (toml lib is read-only)
|
|
374
997
|
const isToml = found.format === "toml";
|
|
375
998
|
const result = migrateConfig(found.parsed);
|
|
376
|
-
const { migrated, warnings } = result;
|
|
999
|
+
const { migrated, warnings, advisories } = result;
|
|
377
1000
|
const changes = [...result.changes];
|
|
378
1001
|
if (found.rootDirectoriesMoved) {
|
|
379
1002
|
changes.push({ key: "directories", description: "moved package.json root-level directories → build.directories" });
|
|
380
1003
|
}
|
|
381
1004
|
const modified = result.modified || found.rootDirectoriesMoved === true;
|
|
382
|
-
if (!modified) {
|
|
383
|
-
log.info(null, "config is already up to date — no changes needed");
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
1005
|
for (const change of changes) {
|
|
387
1006
|
log.info({ key: change.key }, change.description);
|
|
388
1007
|
}
|
|
389
1008
|
for (const warning of warnings) {
|
|
390
1009
|
log.warn(null, warning);
|
|
391
1010
|
}
|
|
1011
|
+
// Advisories are informational and surface even when nothing changed; they never cause a rewrite.
|
|
1012
|
+
for (const advisory of advisories) {
|
|
1013
|
+
log.warn(null, advisory);
|
|
1014
|
+
}
|
|
1015
|
+
if (!modified) {
|
|
1016
|
+
// A kept key (e.g. a customNsisBinary with a url) leaves the config unmodified but is not "up to date".
|
|
1017
|
+
if (advisories.length === 0 && warnings.length === 0) {
|
|
1018
|
+
log.info(null, "config is already up to date — no changes needed");
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
392
1022
|
if (isToml) {
|
|
393
1023
|
log.warn({ file: location }, "TOML configs cannot be automatically rewritten; apply the changes above manually");
|
|
394
1024
|
return;
|
|
@@ -403,23 +1033,81 @@ export async function migrateSchema(args) {
|
|
|
403
1033
|
log.warn(null, "JSON5 was re-serialized as JSON — comments were not preserved");
|
|
404
1034
|
}
|
|
405
1035
|
}
|
|
1036
|
+
async function migrateProgrammaticConfigFile(found, location, dryRun) {
|
|
1037
|
+
if (loadTypeScript() == null) {
|
|
1038
|
+
log.warn({ file: location }, "auto-migration of programmatic configs requires the 'typescript' package, which was not found. Install it (e.g. `npm i -D typescript`) or apply these changes manually:");
|
|
1039
|
+
printManualSteps();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const result = migrateProgrammaticSource(found.rawText, found.configFile);
|
|
1043
|
+
const printAdvisories = () => {
|
|
1044
|
+
for (const advisory of result.advisories) {
|
|
1045
|
+
log.warn(null, advisory);
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
if (result.status === "unsupported") {
|
|
1049
|
+
log.warn({ file: location, reason: result.unsupportedReason }, "this programmatic config could not be auto-migrated; apply these changes manually:");
|
|
1050
|
+
printManualSteps();
|
|
1051
|
+
printAdvisories();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (result.status === "no-op") {
|
|
1055
|
+
// A no-op can still carry warnings for keys that cannot be rewritten (e.g. customSquirrelVendorDir).
|
|
1056
|
+
for (const warning of result.warnings) {
|
|
1057
|
+
log.warn(null, warning);
|
|
1058
|
+
}
|
|
1059
|
+
if (result.advisories.length === 0 && result.warnings.length === 0) {
|
|
1060
|
+
log.info(null, "config is already up to date — no changes needed");
|
|
1061
|
+
}
|
|
1062
|
+
printAdvisories();
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
for (const change of result.changes) {
|
|
1066
|
+
log.info({ key: change.key }, change.description);
|
|
1067
|
+
}
|
|
1068
|
+
for (const warning of result.warnings) {
|
|
1069
|
+
log.warn(null, warning);
|
|
1070
|
+
}
|
|
1071
|
+
printAdvisories();
|
|
1072
|
+
if (dryRun) {
|
|
1073
|
+
log.info(null, "dry run — no files written");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
await fs.writeFile(found.configFile, result.code, "utf8");
|
|
1077
|
+
log.info({ file: location }, "wrote migrated config");
|
|
1078
|
+
}
|
|
406
1079
|
function printManualSteps() {
|
|
407
1080
|
const steps = [
|
|
1081
|
+
"",
|
|
408
1082
|
"• Remove electronCompile",
|
|
409
1083
|
"• Remove framework, nodeVersion, launchUiVersion",
|
|
410
1084
|
"• Rename npmSkipBuildFromSource → buildDependenciesFromSource",
|
|
411
|
-
|
|
412
|
-
"• Rename asar-unpack / asar-unpack-dir / asar.
|
|
1085
|
+
'• Move buildDependenciesFromSource, nodeGypRebuild, npmRebuild, nativeRebuilder into nativeModules (rename nativeRebuilder → rebuildMode; drop a "legacy" value)',
|
|
1086
|
+
"• Rename asar-unpack / asar-unpack-dir / asar.unpackDir → asarUnpack; then move asarUnpack → asar.unpack",
|
|
1087
|
+
"• Move mac/mas/masDev/win/linux asarUnpack → <platform>.asar.unpack (a platform-level asar replaces the root one, so copy the root asar options into it)",
|
|
413
1088
|
"• Move disableSanityCheckAsar → asar.disableSanityCheck",
|
|
414
1089
|
"• Move disableAsarIntegrity → asar.disableIntegrity",
|
|
415
|
-
"• Replace asar: true with an asar object (e.g. asar: {})",
|
|
1090
|
+
"• Replace asar: true with an asar object (e.g. asar: {} or omit entirely - asar is enabled by default)",
|
|
416
1091
|
"• Remove appImage.systemIntegration",
|
|
1092
|
+
"• Remove linux.syncDesktopName (the installed .desktop filename is always synced from desktopName in v27)",
|
|
417
1093
|
"• Rename snap → snapcraft; nest options under a base-named sub-key (default base: core20)",
|
|
418
|
-
"• Replace vPrefixedTagName with tagNamePrefix on GitHub publish entries",
|
|
419
|
-
"• Move
|
|
1094
|
+
"• Replace vPrefixedTagName with tagNamePrefix on GitHub publish entries ('v' is the default prefix - just like before - but can now be customized with tagNamePrefix); an empty tagNamePrefix is now honored",
|
|
1095
|
+
"• Move win.signtoolOptions → win.sign: { type: 'signtool', ...fields }",
|
|
1096
|
+
"• Move win.azureSignOptions → win.sign: { type: 'azure', ...fields }; move untyped extra keys into win.sign.additionalMetadata",
|
|
1097
|
+
"• Remove win.signAndEditExecutable (resource editing always runs in v27); replace win.signExecutable: false with win.sign: false",
|
|
420
1098
|
"• Move helper-bundle-id → mac.helperBundleId",
|
|
421
1099
|
"• Replace squirrelWindows.noMsi with squirrelWindows.msi (inverted)",
|
|
1100
|
+
"• Replace squirrelWindows.customSquirrelVendorDir with a toolsets.squirrel custom bundle (it must contain an electron-winstaller/vendor/ subtree)",
|
|
1101
|
+
"• Move nsis/nsisWeb customNsisBinary.debugLogging → installerDebugLogging (it needs a log-enabled custom toolsets.nsis); remove customNsisBinary and customNsisResources, replacing a custom url/checksum with a toolsets.nsis custom bundle (the checksum carries over; the bundle must also contain the NSIS plugins)",
|
|
1102
|
+
"• Move mac/mas/masDev signing fields (identity, entitlements, entitlementsInherit, entitlementsLoginHelper, hardenedRuntime, type, requirements, timestamp, binaries, strictVerify, preAutoEntitlements, provisioningProfile, additionalArguments) into the `sign` object; rename signIgnore → sign.ignore; remove gatekeeperAssess",
|
|
1103
|
+
"• Move mac/mas/masDev mergeASARs / singleArchFiles / x64ArchFiles into the `universal` object",
|
|
1104
|
+
"• Rename electronDownload → electronGet (mirror → mirrorOptions.mirror; isVerifyChecksum:false → unsafelyDisableChecksums:true; drop cache/customDir/customFilename/strictSSL/platform/arch/version/force)",
|
|
1105
|
+
'• Remove toolsets.* entries set to null (unset resolves to "latest"); replace toolsets.appimage "1.0.2" with "1.0.3"',
|
|
422
1106
|
"• Move root-level package.json directories → build.directories",
|
|
1107
|
+
"",
|
|
1108
|
+
"• For programmatic configs (JS/TS/MJS/CJS), apply the above changes manually to your config object",
|
|
1109
|
+
"• For additional guidance, check the migration walkthrough: https://www.electron.build/docs/migration/v26-to-v27",
|
|
1110
|
+
"• Full list of breaking changes: https://www.electron.build/docs/migration/v27-breaking-changes",
|
|
423
1111
|
];
|
|
424
1112
|
for (const step of steps) {
|
|
425
1113
|
process.stdout.write(` ${step}\n`);
|