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
|
@@ -0,0 +1,1581 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { NSIS_CONFIG_KEYS } from "app-builder-lib/internal";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { ASAR_PLATFORM_KEYS, AZURE_KNOWN_FIELDS, ELECTRON_DOWNLOAD_DROPPED, FEED_QUERY_ADVISORY, isLegacyElectronDownloadKey, legacyKeyMessage, MAC_ENTITLEMENTS_ADVISORY, MAC_NULL_MEANS_UNSET_FIELDS, MAC_SIGN_FIELDS, MAC_SIGN_REMOVED_FIELDS, MAC_UNIVERSAL_FIELDS, normalizeTargetName, NSIS_PER_MACHINE_UPDATE_ADVISORY, NSIS_WEB_ADVISORY, PORTABLE_DEBUG_LOGGING_DROPPED, SNAP_BASES, SNAP_CORE24_UNSUPPORTED, WIN_SIGN_HOOK_PUBLISHER_NAME_ADVISORY, } from "./migrate-schema.js";
|
|
5
|
+
const _require = createRequire(import.meta.url);
|
|
6
|
+
// `typescript` is intentionally NOT a dependency of electron-builder — it is loaded lazily (same
|
|
7
|
+
// pattern as js-yaml/json5/toml) and the feature degrades to manual steps when it is unavailable.
|
|
8
|
+
// Types are kept loose (`any`) on purpose so no `typescript` types are required at compile time.
|
|
9
|
+
let tsModuleCache;
|
|
10
|
+
export function loadTypeScript() {
|
|
11
|
+
if (tsModuleCache !== undefined) {
|
|
12
|
+
return tsModuleCache;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
tsModuleCache = _require("typescript");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
tsModuleCache = null;
|
|
19
|
+
}
|
|
20
|
+
return tsModuleCache;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Migrates a programmatic JS/TS electron-builder config (the same v26→v27 transforms as
|
|
24
|
+
* {@link migrateConfig}) by surgically rewriting the source text. The TypeScript parser is used only
|
|
25
|
+
* to locate node ranges; everything outside an edited range is preserved byte-for-byte, so comments,
|
|
26
|
+
* imports, functions, and formatting are untouched.
|
|
27
|
+
*
|
|
28
|
+
* Pure: source string in, result out. No file I/O. Returns status "unsupported" (with the source
|
|
29
|
+
* unchanged) when typescript is not installed or the config cannot be statically reduced to a single
|
|
30
|
+
* object literal (function that builds the object dynamically, spreads, computed keys, …).
|
|
31
|
+
*/
|
|
32
|
+
export function migrateProgrammaticSource(sourceText, fileName) {
|
|
33
|
+
const ts = loadTypeScript();
|
|
34
|
+
if (ts == null) {
|
|
35
|
+
return { code: sourceText, changes: [], warnings: [], advisories: [], status: "unsupported", unsupportedReason: "typescript-not-installed" };
|
|
36
|
+
}
|
|
37
|
+
const scriptKind = scriptKindFor(ts, fileName);
|
|
38
|
+
const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, /* setParentNodes */ true, scriptKind);
|
|
39
|
+
const codemod = new ConfigCodemod(ts, sf, sourceText);
|
|
40
|
+
const located = codemod.locateConfigObject();
|
|
41
|
+
if (located.objLit == null) {
|
|
42
|
+
return { code: sourceText, changes: [], warnings: [], advisories: [], status: "unsupported", unsupportedReason: located.reason };
|
|
43
|
+
}
|
|
44
|
+
codemod.run(located.objLit);
|
|
45
|
+
if (codemod.edits.length === 0) {
|
|
46
|
+
return { code: sourceText, changes: codemod.changes, warnings: codemod.warnings, advisories: codemod.advisories, status: "no-op" };
|
|
47
|
+
}
|
|
48
|
+
return { code: codemod.apply(), changes: codemod.changes, warnings: codemod.warnings, advisories: codemod.advisories, status: "migrated" };
|
|
49
|
+
}
|
|
50
|
+
function scriptKindFor(ts, fileName) {
|
|
51
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
52
|
+
switch (ext) {
|
|
53
|
+
case ".ts":
|
|
54
|
+
case ".cts":
|
|
55
|
+
case ".mts":
|
|
56
|
+
return ts.ScriptKind.TS;
|
|
57
|
+
case ".tsx":
|
|
58
|
+
return ts.ScriptKind.TSX;
|
|
59
|
+
case ".jsx":
|
|
60
|
+
return ts.ScriptKind.JSX;
|
|
61
|
+
default:
|
|
62
|
+
return ts.ScriptKind.JS;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
class ConfigCodemod {
|
|
66
|
+
constructor(ts, sf, text) {
|
|
67
|
+
this.ts = ts;
|
|
68
|
+
this.sf = sf;
|
|
69
|
+
this.text = text;
|
|
70
|
+
this.edits = [];
|
|
71
|
+
this.changes = [];
|
|
72
|
+
this.warnings = [];
|
|
73
|
+
this.advisories = [];
|
|
74
|
+
this.indentUnit = " ";
|
|
75
|
+
}
|
|
76
|
+
// ── Locate the config object literal ──────────────────────────────────────
|
|
77
|
+
locateConfigObject() {
|
|
78
|
+
const ts = this.ts;
|
|
79
|
+
const candidates = [];
|
|
80
|
+
for (const stmt of this.sf.statements) {
|
|
81
|
+
if (ts.isExportAssignment(stmt)) {
|
|
82
|
+
// `export default <expr>` and `export = <expr>`
|
|
83
|
+
candidates.push(stmt.expression);
|
|
84
|
+
}
|
|
85
|
+
else if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
86
|
+
const lhs = stmt.expression.left;
|
|
87
|
+
if (this.isModuleExportsTarget(lhs)) {
|
|
88
|
+
candidates.push(stmt.expression.right);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (ts.isFunctionDeclaration(stmt) && this.hasDefaultExport(stmt)) {
|
|
92
|
+
// `export default function () { return {...} }`
|
|
93
|
+
const lit = this.literalFromFunctionLike(stmt);
|
|
94
|
+
if (lit != null) {
|
|
95
|
+
candidates.push(lit);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Inline `build({ config: {...} })` form (programmatic-usage.md). Search the whole tree.
|
|
100
|
+
this.forEachDescendant(this.sf, node => {
|
|
101
|
+
const cfg = this.configArgOfBuildCall(node);
|
|
102
|
+
if (cfg != null) {
|
|
103
|
+
candidates.push(cfg);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
let reason = "no electron-builder config object literal found (expected a default/module.exports object export or a build({ config }) call)";
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
const resolved = this.resolveToObjectLiteral(candidate, 0);
|
|
109
|
+
if (resolved.objLit != null) {
|
|
110
|
+
return { objLit: resolved.objLit };
|
|
111
|
+
}
|
|
112
|
+
if (resolved.reason != null) {
|
|
113
|
+
reason = resolved.reason;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { reason };
|
|
117
|
+
}
|
|
118
|
+
isModuleExportsTarget(lhs) {
|
|
119
|
+
const ts = this.ts;
|
|
120
|
+
if (!ts.isPropertyAccessExpression(lhs)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
// module.exports = ... OR exports.default = ...
|
|
124
|
+
if (ts.isIdentifier(lhs.expression) && lhs.expression.text === "module" && lhs.name.text === "exports") {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (ts.isIdentifier(lhs.expression) && lhs.expression.text === "exports" && lhs.name.text === "default") {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
hasDefaultExport(node) {
|
|
133
|
+
const ts = this.ts;
|
|
134
|
+
const mods = node.modifiers;
|
|
135
|
+
if (mods == null) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
let hasExport = false;
|
|
139
|
+
let hasDefault = false;
|
|
140
|
+
for (const m of mods) {
|
|
141
|
+
if (m.kind === ts.SyntaxKind.ExportKeyword) {
|
|
142
|
+
hasExport = true;
|
|
143
|
+
}
|
|
144
|
+
if (m.kind === ts.SyntaxKind.DefaultKeyword) {
|
|
145
|
+
hasDefault = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return hasExport && hasDefault;
|
|
149
|
+
}
|
|
150
|
+
configArgOfBuildCall(node) {
|
|
151
|
+
const ts = this.ts;
|
|
152
|
+
if (!ts.isCallExpression(node)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const callee = node.expression;
|
|
156
|
+
const isBuild = (ts.isIdentifier(callee) && callee.text === "build") || (ts.isPropertyAccessExpression(callee) && callee.name.text === "build");
|
|
157
|
+
if (!isBuild || node.arguments.length === 0) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const arg0 = node.arguments[0];
|
|
161
|
+
if (!ts.isObjectLiteralExpression(arg0)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const configProp = this.getProp(arg0, "config");
|
|
165
|
+
return configProp != null && ts.isPropertyAssignment(configProp) ? configProp.initializer : null;
|
|
166
|
+
}
|
|
167
|
+
resolveToObjectLiteral(expr, depth) {
|
|
168
|
+
const ts = this.ts;
|
|
169
|
+
if (depth > 6) {
|
|
170
|
+
return { reason: "config export could not be resolved (too many indirections)" };
|
|
171
|
+
}
|
|
172
|
+
const node = this.unwrap(expr);
|
|
173
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
174
|
+
if (this.hasSpreadOrComputed(node)) {
|
|
175
|
+
return { reason: "config object uses a spread (`...`) or computed key, which cannot be migrated automatically" };
|
|
176
|
+
}
|
|
177
|
+
return { objLit: node };
|
|
178
|
+
}
|
|
179
|
+
if (ts.isIdentifier(node)) {
|
|
180
|
+
const init = this.findVariableInitializer(node.text);
|
|
181
|
+
if (init == null) {
|
|
182
|
+
return { reason: `config export references "${node.text}", which is not a local object literal` };
|
|
183
|
+
}
|
|
184
|
+
return this.resolveToObjectLiteral(init, depth + 1);
|
|
185
|
+
}
|
|
186
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
187
|
+
const lit = this.literalFromFunctionLike(node);
|
|
188
|
+
if (lit == null) {
|
|
189
|
+
return { reason: "config is a function that does not return a single object literal — migrate it manually" };
|
|
190
|
+
}
|
|
191
|
+
return this.resolveToObjectLiteral(lit, depth + 1);
|
|
192
|
+
}
|
|
193
|
+
return { reason: "config export is not an object literal — migrate it manually" };
|
|
194
|
+
}
|
|
195
|
+
/** Unwraps parentheses and type-only wrappers (`as`, `satisfies`, `!`, `<T>x`) to the inner expression. */
|
|
196
|
+
unwrap(node) {
|
|
197
|
+
const ts = this.ts;
|
|
198
|
+
let n = node;
|
|
199
|
+
for (;;) {
|
|
200
|
+
if (ts.isParenthesizedExpression(n) ||
|
|
201
|
+
ts.isAsExpression(n) ||
|
|
202
|
+
(ts.isSatisfiesExpression && ts.isSatisfiesExpression(n)) ||
|
|
203
|
+
ts.isNonNullExpression(n) ||
|
|
204
|
+
ts.isTypeAssertionExpression?.(n)) {
|
|
205
|
+
n = n.expression;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
return n;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
hasSpreadOrComputed(objLit) {
|
|
213
|
+
const ts = this.ts;
|
|
214
|
+
for (const p of objLit.properties) {
|
|
215
|
+
if (ts.isSpreadAssignment(p)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (p.name != null && ts.isComputedPropertyName(p.name)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
findVariableInitializer(name) {
|
|
225
|
+
const ts = this.ts;
|
|
226
|
+
for (const stmt of this.sf.statements) {
|
|
227
|
+
if (!ts.isVariableStatement(stmt)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
231
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === name && decl.initializer != null) {
|
|
232
|
+
return decl.initializer;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
/** Returns the single object-literal expression a function-like trivially yields, or null. */
|
|
239
|
+
literalFromFunctionLike(fn) {
|
|
240
|
+
const ts = this.ts;
|
|
241
|
+
const body = fn.body;
|
|
242
|
+
if (body == null) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
if (!ts.isBlock(body)) {
|
|
246
|
+
// arrow expression body: `() => ({...})` / `() => obj`
|
|
247
|
+
return body;
|
|
248
|
+
}
|
|
249
|
+
const returns = [];
|
|
250
|
+
this.collectReturns(body, returns);
|
|
251
|
+
if (returns.length !== 1 || returns[0].expression == null) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return returns[0].expression;
|
|
255
|
+
}
|
|
256
|
+
collectReturns(node, out) {
|
|
257
|
+
const ts = this.ts;
|
|
258
|
+
const visit = (n) => {
|
|
259
|
+
// Don't descend into nested functions — their returns are not the config's.
|
|
260
|
+
if (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (ts.isReturnStatement(n)) {
|
|
264
|
+
out.push(n);
|
|
265
|
+
}
|
|
266
|
+
n.forEachChild(visit);
|
|
267
|
+
};
|
|
268
|
+
node.forEachChild(visit);
|
|
269
|
+
}
|
|
270
|
+
forEachDescendant(node, cb) {
|
|
271
|
+
const visit = (n) => {
|
|
272
|
+
cb(n);
|
|
273
|
+
n.forEachChild(visit);
|
|
274
|
+
};
|
|
275
|
+
node.forEachChild(visit);
|
|
276
|
+
}
|
|
277
|
+
// ── Run all rules ─────────────────────────────────────────────────────────
|
|
278
|
+
run(root) {
|
|
279
|
+
this.indentUnit = this.detectIndentUnit(root);
|
|
280
|
+
this.ruleRemoveKeys(root, ["electronCompile"], "removed electronCompile (unsupported since v27; migrate to electron-vite, esbuild, or webpack)");
|
|
281
|
+
this.ruleRemoveKeys(root, ["framework", "nodeVersion", "launchUiVersion"], key => `removed ${key} (Electron is the only supported framework in v27)`);
|
|
282
|
+
const disableDefaultIgnoredFilesDesc = "removed disableDefaultIgnoredFiles (in v27, include a default-excluded file via an explicit `files` glob, e.g. `**/*.obj`)";
|
|
283
|
+
this.ruleRemoveKeys(root, ["disableDefaultIgnoredFiles"], disableDefaultIgnoredFilesDesc);
|
|
284
|
+
// mas/masDev are MacConfiguration-derived, so they accept the (now-removed) key too.
|
|
285
|
+
for (const platform of ["mac", "mas", "masDev", "win", "linux"]) {
|
|
286
|
+
const p = this.getObjectProp(root, platform);
|
|
287
|
+
if (p != null) {
|
|
288
|
+
this.ruleRemoveKeys(p, ["disableDefaultIgnoredFiles"], disableDefaultIgnoredFilesDesc);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
this.ruleSyncDesktopName(root);
|
|
292
|
+
this.ruleNativeModules(root);
|
|
293
|
+
// Platform asar reads the root keys before ruleAsar edits them (edits only apply at the end, so order is for clarity).
|
|
294
|
+
for (const platform of ASAR_PLATFORM_KEYS) {
|
|
295
|
+
const p = this.getObjectProp(root, platform);
|
|
296
|
+
if (p != null) {
|
|
297
|
+
this.rulePlatformAsar(root, p, platform);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
this.ruleAsar(root);
|
|
301
|
+
this.ruleAppImageSystemIntegration(root);
|
|
302
|
+
this.rulePublish(root, "publish");
|
|
303
|
+
// publish nested in any platform/target section (mac, win, nsis, dmg, …). `snap` is skipped: ruleSnap
|
|
304
|
+
// re-emits its properties verbatim, which would overlap an edit made inside it.
|
|
305
|
+
for (const prop of root.properties) {
|
|
306
|
+
const name = this.propName(prop);
|
|
307
|
+
const obj = name != null && name !== "snap" ? this.getObjectProp(root, name) : null;
|
|
308
|
+
if (obj != null) {
|
|
309
|
+
this.rulePublish(obj, "publish", name + ".");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.ruleSnap(root);
|
|
313
|
+
this.ruleHelperBundleId(root);
|
|
314
|
+
this.ruleSquirrelNoMsi(root);
|
|
315
|
+
this.ruleCustomNsis(root);
|
|
316
|
+
this.ruleWinSign(root);
|
|
317
|
+
for (const platform of ["mac", "mas", "masDev"]) {
|
|
318
|
+
const p = this.getObjectProp(root, platform);
|
|
319
|
+
if (p != null) {
|
|
320
|
+
this.ruleMacSigning(p, platform);
|
|
321
|
+
this.ruleMacUniversal(p, platform);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.ruleElectronDownload(root);
|
|
325
|
+
this.ruleToolsets(root);
|
|
326
|
+
this.ruleNsisWebAdvisory(root);
|
|
327
|
+
this.ruleMacEntitlementsAdvisory(root);
|
|
328
|
+
this.ruleNsisPerMachineAdvisory(root);
|
|
329
|
+
this.ruleWinSignHookPublisherNameAdvisory(root);
|
|
330
|
+
this.ruleFeedQueryAdvisory(root);
|
|
331
|
+
}
|
|
332
|
+
// ── Rules ───────────────────────────────────────────────────────────────
|
|
333
|
+
// Advisory only — adds no edit, so an nsis-web-only config stays a "no-op" and is never rewritten.
|
|
334
|
+
ruleNsisWebAdvisory(root) {
|
|
335
|
+
if (this.detectTarget(root, "win", ["nsis-web"])) {
|
|
336
|
+
this.advisories.push(NSIS_WEB_ADVISORY);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Advisory only. Mirrors migrateConfig: a mac/mas/masDev object that names no entitlements file (at the v26
|
|
340
|
+
// location or under `sign`) relies on the bundled default, which v27 tightened.
|
|
341
|
+
ruleMacEntitlementsAdvisory(root) {
|
|
342
|
+
const ts = this.ts;
|
|
343
|
+
const isNullLiteral = (prop) => prop != null && ts.isPropertyAssignment(prop) && this.unwrap(prop.initializer).kind === ts.SyntaxKind.NullKeyword;
|
|
344
|
+
const namesEntitlements = (obj) => {
|
|
345
|
+
const prop = obj == null ? null : this.getProp(obj, "entitlements");
|
|
346
|
+
return prop != null && !isNullLiteral(prop);
|
|
347
|
+
};
|
|
348
|
+
// Not signing with electron-builder's defaults at all: a custom signer, or identity: null (skip signing).
|
|
349
|
+
const bypassesDefaults = (p, sign) => {
|
|
350
|
+
const signProp = this.getProp(p, "sign");
|
|
351
|
+
const custom = signProp != null && ts.isPropertyAssignment(signProp) && sign == null && !isNullLiteral(signProp);
|
|
352
|
+
return custom || isNullLiteral(this.getProp(p, "identity")) || (sign != null && isNullLiteral(this.getProp(sign, "identity")));
|
|
353
|
+
};
|
|
354
|
+
for (const platform of ["mac", "mas", "masDev"]) {
|
|
355
|
+
const p = this.getObjectProp(root, platform);
|
|
356
|
+
const sign = p == null ? null : this.getObjectProp(p, "sign");
|
|
357
|
+
if (p != null && !namesEntitlements(p) && !namesEntitlements(sign) && !bypassesDefaults(p, sign)) {
|
|
358
|
+
this.advisories.push(MAC_ENTITLEMENTS_ADVISORY);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Advisory only. Mirrors migrateConfig: nsis.perMachine or nsisWeb.perMachine is literally true (a non-literal value is not read).
|
|
364
|
+
ruleNsisPerMachineAdvisory(root) {
|
|
365
|
+
const ts = this.ts;
|
|
366
|
+
if (["nsis", "nsisWeb"].some(name => this.propValue(this.objectLiteral(this.propValue(root, name)), "perMachine")?.kind === ts.SyntaxKind.TrueKeyword)) {
|
|
367
|
+
this.advisories.push(NSIS_PER_MACHINE_UPDATE_ADVISORY);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Advisory only. Mirrors migrateConfig on the win.sign it produces: win.sign when set, otherwise the v26 win.azureSignOptions
|
|
371
|
+
// (else win.signtoolOptions) moved into it, unless signExecutable / signAndEditExecutable: false disabled signing.
|
|
372
|
+
ruleWinSignHookPublisherNameAdvisory(root) {
|
|
373
|
+
const ts = this.ts;
|
|
374
|
+
const isNullish = (v) => v === undefined || v.kind === ts.SyntaxKind.NullKeyword;
|
|
375
|
+
// A non-literal flag may be false, so it counts as false (no advisory).
|
|
376
|
+
const isFalseOrUnread = (v) => v !== undefined && (v.kind === ts.SyntaxKind.FalseKeyword || !this.isLiteral(v));
|
|
377
|
+
const win = this.objectLiteral(this.propValue(root, "win"));
|
|
378
|
+
if (win == null) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const winSign = this.propValue(win, "sign");
|
|
382
|
+
let legacy = null;
|
|
383
|
+
let sign = null;
|
|
384
|
+
if (!isNullish(winSign)) {
|
|
385
|
+
sign = this.objectLiteral(winSign);
|
|
386
|
+
}
|
|
387
|
+
else if (!["signExecutable", "signAndEditExecutable"].some(k => isFalseOrUnread(this.propValue(win, k)))) {
|
|
388
|
+
const azure = this.propValue(win, "azureSignOptions");
|
|
389
|
+
legacy = isNullish(azure) ? "signtool" : "azure";
|
|
390
|
+
sign = this.objectLiteral(legacy === "azure" ? azure : this.propValue(win, "signtoolOptions"));
|
|
391
|
+
}
|
|
392
|
+
// A sign value that is not an object literal may carry publisherName or a certificate (a spread in one counts as a publisherName below).
|
|
393
|
+
if (sign == null) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// migrateConfig moves a string-valued azureSignOptions field that is not a known Azure option into additionalMetadata.
|
|
397
|
+
const field = (name) => {
|
|
398
|
+
const v = this.propValue(sign, name);
|
|
399
|
+
return legacy === "azure" && v !== undefined && !AZURE_KNOWN_FIELDS.has(name) && (ts.isStringLiteralLike(v) || ts.isTemplateExpression(v)) ? undefined : v;
|
|
400
|
+
};
|
|
401
|
+
// Any value but null or a non-string literal counts as a hook: a JS config usually passes a function or an imported module.
|
|
402
|
+
const hook = field("sign");
|
|
403
|
+
if (isNullish(hook) || (this.isLiteral(hook) && !ts.isStringLiteralLike(hook))) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// Mere presence suppresses: publisherName with any value (null too). A certificate in the config doesn't: a hook needs publisherName.
|
|
407
|
+
if (field("publisherName") !== undefined || isFalseOrUnread(this.propValue(win, "verifyUpdateCodeSignature"))) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.advisories.push(WIN_SIGN_HOOK_PUBLISHER_NAME_ADVISORY);
|
|
411
|
+
}
|
|
412
|
+
// Advisory only. Mirrors migrateConfig, which walks the publish of the root and of each top-level section of the migrated config:
|
|
413
|
+
// a v26 `snap` moves under snapcraft.<base> (not walked) unless its base is "custom", when its keys replace snapcraft's.
|
|
414
|
+
ruleFeedQueryAdvisory(root) {
|
|
415
|
+
const ts = this.ts;
|
|
416
|
+
const snap = this.objectLiteral(this.propValue(root, "snap"));
|
|
417
|
+
const snapBase = this.propValue(snap, "base");
|
|
418
|
+
const snapIsCustom = snapBase !== undefined && ts.isStringLiteral(snapBase) && snapBase.text === "custom";
|
|
419
|
+
// A base the AST cannot read may be "custom" too, so snapcraft.publish is not read when the snap may replace it.
|
|
420
|
+
const snapReplacesPublish = (snapIsCustom || (snapBase !== undefined && !this.isLiteral(snapBase))) && this.propValue(snap, "publish") !== undefined;
|
|
421
|
+
const sections = [root];
|
|
422
|
+
for (const prop of root.properties) {
|
|
423
|
+
const name = this.propName(prop);
|
|
424
|
+
const section = name == null || name === "snap" ? null : this.objectLiteral(this.propValue(root, name));
|
|
425
|
+
if (section != null && !(name === "snapcraft" && snapReplacesPublish)) {
|
|
426
|
+
sections.push(section);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (snapIsCustom) {
|
|
430
|
+
sections.push(snap);
|
|
431
|
+
}
|
|
432
|
+
// A non-literal url is not read, but a template's static text is: a "?" there is in every url the template produces.
|
|
433
|
+
const urlHasQuery = (url) => url !== undefined &&
|
|
434
|
+
(ts.isStringLiteralLike(url)
|
|
435
|
+
? url.text.includes("?")
|
|
436
|
+
: ts.isTemplateExpression(url) && [url.head, ...url.templateSpans.map((s) => s.literal)].some((part) => part.text.includes("?")));
|
|
437
|
+
const isGenericWithQuery = (entry) => {
|
|
438
|
+
const obj = this.objectLiteral(this.unwrap(entry));
|
|
439
|
+
const provider = this.propValue(obj, "provider");
|
|
440
|
+
return provider !== undefined && ts.isStringLiteralLike(provider) && provider.text === "generic" && urlHasQuery(this.propValue(obj, "url"));
|
|
441
|
+
};
|
|
442
|
+
for (const section of sections) {
|
|
443
|
+
const publish = this.propValue(section, "publish");
|
|
444
|
+
const entries = publish === undefined ? [] : ts.isArrayLiteralExpression(publish) ? publish.elements : [publish];
|
|
445
|
+
if (entries.some(isGenericWithQuery)) {
|
|
446
|
+
this.advisories.push(FEED_QUERY_ADVISORY);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
ruleToolsets(root) {
|
|
452
|
+
const ts = this.ts;
|
|
453
|
+
const toolsets = this.getObjectProp(root, "toolsets");
|
|
454
|
+
if (toolsets == null) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
for (const prop of toolsets.properties) {
|
|
458
|
+
const name = this.propName(prop);
|
|
459
|
+
if (name == null || !ts.isPropertyAssignment(prop)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const value = this.unwrap(prop.initializer);
|
|
463
|
+
if (value.kind === ts.SyntaxKind.NullKeyword) {
|
|
464
|
+
this.removeProp(prop);
|
|
465
|
+
this.changes.push({
|
|
466
|
+
key: `toolsets.${name}`,
|
|
467
|
+
description: `removed toolsets.${name}: null (v27 rejects null; unset resolves to "latest", the newest bundle — pin "0.0.0" to keep the legacy bundle v26 used)`,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
else if (name === "appimage" && ts.isStringLiteral(value) && value.text === "1.0.2") {
|
|
471
|
+
this.replaceValue(prop.initializer, '"1.0.3"');
|
|
472
|
+
this.changes.push({
|
|
473
|
+
key: "toolsets.appimage",
|
|
474
|
+
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)',
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** Mirrors migrateConfig's detectTarget: `<platform>.target` or the root target selects one of `names`. */
|
|
480
|
+
detectTarget(root, platform, names) {
|
|
481
|
+
const platformTarget = this.propValue(this.objectLiteral(this.propValue(root, platform)), "target");
|
|
482
|
+
return this.hasTargetIn(platformTarget, names) || this.hasTargetIn(this.propValue(root, "target"), names);
|
|
483
|
+
}
|
|
484
|
+
/** Mirrors migrateConfig's hasTarget: a target name (with an optional ":<arch>" suffix, any case), a `{ target }` object, or an array of either. */
|
|
485
|
+
hasTargetIn(value, names) {
|
|
486
|
+
const ts = this.ts;
|
|
487
|
+
if (value === undefined) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
// A non-literal name (identifier, template with substitutions, …) is not read.
|
|
491
|
+
if (ts.isStringLiteralLike(value)) {
|
|
492
|
+
return names.includes(normalizeTargetName(value.text));
|
|
493
|
+
}
|
|
494
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
495
|
+
return value.elements.some((el) => this.hasTargetIn(this.unwrap(el), names));
|
|
496
|
+
}
|
|
497
|
+
return this.hasTargetIn(this.propValue(this.objectLiteral(value), "target"), names);
|
|
498
|
+
}
|
|
499
|
+
ruleRemoveKeys(obj, keys, description) {
|
|
500
|
+
for (const key of keys) {
|
|
501
|
+
const prop = this.getProp(obj, key);
|
|
502
|
+
if (prop != null) {
|
|
503
|
+
this.removeProp(prop);
|
|
504
|
+
this.changes.push({ key, description: typeof description === "function" ? description(key) : description });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// linux.syncDesktopName was removed in v27 (desktop-name syncing is now always on). Remove the key;
|
|
509
|
+
// warn when it was explicitly false, since that changed behaviour (the installed .desktop filename now syncs).
|
|
510
|
+
ruleSyncDesktopName(root) {
|
|
511
|
+
const ts = this.ts;
|
|
512
|
+
const linux = this.getObjectProp(root, "linux");
|
|
513
|
+
if (linux == null) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const prop = this.getProp(linux, "syncDesktopName");
|
|
517
|
+
if (prop == null || !ts.isPropertyAssignment(prop)) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
521
|
+
this.warnings.push("linux.syncDesktopName: false disabled desktop-name syncing in v26. In v27 the installed .desktop filename is always derived from `desktopName` " +
|
|
522
|
+
"(falling back to executableName). If you relied on the old filename, set `desktopName` explicitly to control it.");
|
|
523
|
+
}
|
|
524
|
+
this.removeProp(prop);
|
|
525
|
+
this.changes.push({
|
|
526
|
+
key: "linux.syncDesktopName",
|
|
527
|
+
description: "removed linux.syncDesktopName (the installed .desktop filename is always synced from desktopName in v27)",
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
ruleNativeModules(root) {
|
|
531
|
+
const sources = [];
|
|
532
|
+
const extraEntries = [];
|
|
533
|
+
for (const key of ["buildDependenciesFromSource", "nodeGypRebuild", "npmRebuild"]) {
|
|
534
|
+
const prop = this.getProp(root, key);
|
|
535
|
+
if (prop != null) {
|
|
536
|
+
sources.push({ prop });
|
|
537
|
+
this.changes.push({ key, description: `moved ${key} → nativeModules.${key}` });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const nativeRebuilder = this.getProp(root, "nativeRebuilder");
|
|
541
|
+
if (nativeRebuilder != null) {
|
|
542
|
+
if (this.stringLiteralValue(nativeRebuilder) === "legacy") {
|
|
543
|
+
// v27 dropped the app-builder binary rebuilder; rebuildMode only selects @electron/rebuild's mode.
|
|
544
|
+
this.removeProp(nativeRebuilder);
|
|
545
|
+
this.warnings.push('nativeRebuilder: "legacy" (the app-builder binary rebuilder) was removed in v27 — native modules are always rebuilt with @electron/rebuild. ' +
|
|
546
|
+
'The value was dropped, so the default nativeModules.rebuildMode ("sequential") applies.');
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
sources.push({ prop: nativeRebuilder, renameTo: "rebuildMode" });
|
|
550
|
+
}
|
|
551
|
+
this.changes.push({ key: "nativeRebuilder", description: "renamed nativeRebuilder → nativeModules.rebuildMode" });
|
|
552
|
+
}
|
|
553
|
+
const npmSkip = this.getProp(root, "npmSkipBuildFromSource");
|
|
554
|
+
if (npmSkip != null && this.ts.isPropertyAssignment(npmSkip)) {
|
|
555
|
+
const hasBds = this.getProp(root, "buildDependenciesFromSource") != null;
|
|
556
|
+
if (!hasBds) {
|
|
557
|
+
extraEntries.push(`buildDependenciesFromSource: ${this.negate(npmSkip.initializer)}`);
|
|
558
|
+
}
|
|
559
|
+
this.removeProp(npmSkip);
|
|
560
|
+
this.changes.push({ key: "npmSkipBuildFromSource", description: "renamed npmSkipBuildFromSource → buildDependenciesFromSource (then grouped under nativeModules)" });
|
|
561
|
+
}
|
|
562
|
+
if (sources.length === 0 && extraEntries.length === 0) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const ok = this.moveInto(root, "nativeModules", sources, extraEntries);
|
|
566
|
+
if (!ok) {
|
|
567
|
+
this.warnings.push("nativeModules already exists but is not an object literal; move buildDependenciesFromSource/nodeGypRebuild/npmRebuild/rebuildMode into it manually.");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
ruleAsar(root) {
|
|
571
|
+
const ts = this.ts;
|
|
572
|
+
const asarProp = this.getProp(root, "asar");
|
|
573
|
+
const asarVal = asarProp != null && ts.isPropertyAssignment(asarProp) ? this.unwrap(asarProp.initializer) : null;
|
|
574
|
+
const asarIsFalse = asarVal != null && asarVal.kind === ts.SyntaxKind.FalseKeyword;
|
|
575
|
+
const asarIsTrue = asarVal != null && asarVal.kind === ts.SyntaxKind.TrueKeyword;
|
|
576
|
+
const asarIsObject = asarVal != null && ts.isObjectLiteralExpression(asarVal);
|
|
577
|
+
if (asarIsFalse) {
|
|
578
|
+
const flatKeys = ["asar-unpack", "asar-unpack-dir", "asarUnpack", "disableSanityCheckAsar", "disableAsarIntegrity"].filter(k => this.getProp(root, k) != null);
|
|
579
|
+
if (flatKeys.length === 0) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (this.platformReEnablesAsar(root)) {
|
|
583
|
+
// Those root values still applied to the re-enabling platform in v26; rulePlatformAsar already warned.
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
for (const key of flatKeys) {
|
|
587
|
+
this.removeProp(this.getProp(root, key));
|
|
588
|
+
this.changes.push({ key, description: `removed ${key} (no effect with asar: false)` });
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// Collect unpack sources (root-level hyphenated/camel + nested asar.unpackDir).
|
|
593
|
+
const unpackSources = [];
|
|
594
|
+
const collectUnpack = (key, descKey, desc) => {
|
|
595
|
+
const prop = this.getProp(root, key);
|
|
596
|
+
if (prop != null && ts.isPropertyAssignment(prop)) {
|
|
597
|
+
unpackSources.push(prop.initializer);
|
|
598
|
+
this.removeProp(prop);
|
|
599
|
+
this.changes.push({ key: descKey, description: desc });
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
collectUnpack("asar-unpack", "asar-unpack", "renamed asar-unpack → asar.unpack");
|
|
603
|
+
collectUnpack("asar-unpack-dir", "asar-unpack-dir", "renamed asar-unpack-dir → asar.unpack");
|
|
604
|
+
collectUnpack("asarUnpack", "asarUnpack", "moved asarUnpack → asar.unpack");
|
|
605
|
+
if (asarIsObject) {
|
|
606
|
+
const nestedUnpackDir = this.getProp(asarVal, "unpackDir");
|
|
607
|
+
if (nestedUnpackDir != null && ts.isPropertyAssignment(nestedUnpackDir)) {
|
|
608
|
+
unpackSources.push(nestedUnpackDir.initializer);
|
|
609
|
+
this.removeProp(nestedUnpackDir);
|
|
610
|
+
this.changes.push({ key: "asar.unpackDir", description: "moved asar.unpackDir → asar.unpack" });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const childRenames = [];
|
|
614
|
+
const collectChild = (key, renameTo, desc) => {
|
|
615
|
+
const prop = this.getProp(root, key);
|
|
616
|
+
if (prop != null && ts.isPropertyAssignment(prop)) {
|
|
617
|
+
childRenames.push({ renameTo, value: prop.initializer });
|
|
618
|
+
this.removeProp(prop);
|
|
619
|
+
this.changes.push({ key, description: desc });
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
collectChild("disableSanityCheckAsar", "disableSanityCheck", "moved disableSanityCheckAsar → asar.disableSanityCheck");
|
|
623
|
+
collectChild("disableAsarIntegrity", "disableIntegrity", "moved disableAsarIntegrity → asar.disableIntegrity");
|
|
624
|
+
if (unpackSources.length === 0 && childRenames.length === 0 && !asarIsTrue) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const buildEntries = (childIndent) => {
|
|
628
|
+
const out = [];
|
|
629
|
+
if (unpackSources.length === 1) {
|
|
630
|
+
out.push(`unpack: ${this.valueText(unpackSources[0], childIndent)}`);
|
|
631
|
+
}
|
|
632
|
+
else if (unpackSources.length > 1) {
|
|
633
|
+
out.push(`unpack: ${this.mergeArrays(unpackSources)}`);
|
|
634
|
+
}
|
|
635
|
+
for (const c of childRenames) {
|
|
636
|
+
out.push(`${c.renameTo}: ${this.valueText(c.value, childIndent)}`);
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
};
|
|
640
|
+
if (asarIsObject) {
|
|
641
|
+
this.insertIntoObject(asarVal, buildEntries(this.propIndentFor(asarVal)));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (asarIsTrue) {
|
|
645
|
+
const braceIndent = this.lineIndentAt(this.start(asarProp.initializer));
|
|
646
|
+
const entries = buildEntries(braceIndent + this.indentUnit);
|
|
647
|
+
if (entries.length === 0) {
|
|
648
|
+
this.removeProp(asarProp);
|
|
649
|
+
this.changes.push({ key: "asar", description: "removed redundant asar: true (asar is enabled by default; the explicit `true` is not needed)" });
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
this.replaceValue(asarProp.initializer, this.objectLiteralTextAt(entries, braceIndent));
|
|
653
|
+
const keys = entries.map(e => e.slice(0, e.indexOf(":")));
|
|
654
|
+
this.changes.push({ key: "asar", description: `replaced asar: true with an asar object carrying ${keys.join(", ")}` });
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// No asar prop yet — create one on root.
|
|
659
|
+
this.createChild(root, "asar", buildEntries(this.propIndentFor(root) + this.indentUnit));
|
|
660
|
+
}
|
|
661
|
+
/** True when some platform section sets its own `asar` to anything but `false` / `null`. */
|
|
662
|
+
platformReEnablesAsar(root) {
|
|
663
|
+
const ts = this.ts;
|
|
664
|
+
return ASAR_PLATFORM_KEYS.some(platform => {
|
|
665
|
+
const p = this.getObjectProp(root, platform);
|
|
666
|
+
const prop = p == null ? null : this.getProp(p, "asar");
|
|
667
|
+
if (prop == null || !ts.isPropertyAssignment(prop)) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
const kind = this.unwrap(prop.initializer).kind;
|
|
671
|
+
return kind !== ts.SyntaxKind.FalseKeyword && kind !== ts.SyntaxKind.NullKeyword;
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Platform-level `asarUnpack` → `<platform>.asar.unpack`. Only the cases where a plain move is exactly the v26
|
|
676
|
+
* result are rewritten: a platform-level `asar` replaces the root one in v27, so whenever root-level asar
|
|
677
|
+
* options exist they would have to be copied into the platform object — that is left to the user, with a warning.
|
|
678
|
+
*/
|
|
679
|
+
rulePlatformAsar(root, platform, name) {
|
|
680
|
+
const ts = this.ts;
|
|
681
|
+
const unpackProp = this.getProp(platform, "asarUnpack");
|
|
682
|
+
const asarProp = this.getProp(platform, "asar");
|
|
683
|
+
const asarVal = asarProp != null && ts.isPropertyAssignment(asarProp) ? this.unwrap(asarProp.initializer) : null;
|
|
684
|
+
const rootAsarProp = this.getProp(root, "asar");
|
|
685
|
+
const rootAsarVal = rootAsarProp != null && ts.isPropertyAssignment(rootAsarProp) ? this.unwrap(rootAsarProp.initializer) : null;
|
|
686
|
+
const rootHasFlat = ["asar-unpack", "asar-unpack-dir", "asarUnpack", "disableSanityCheckAsar", "disableAsarIntegrity"].some(k => this.getProp(root, k) != null) ||
|
|
687
|
+
(rootAsarVal != null && ts.isObjectLiteralExpression(rootAsarVal) && this.getProp(rootAsarVal, "unpackDir") != null);
|
|
688
|
+
const ownIsNull = asarVal == null || asarVal.kind === ts.SyntaxKind.NullKeyword;
|
|
689
|
+
const manual = (why) => this.warnings.push(`${name}.asar: ${why} In v27 a platform-level asar replaces the root one, so copy the root asar options into ${name}.asar` +
|
|
690
|
+
(unpackProp != null ? ` and move ${name}.asarUnpack there manually.` : " manually."));
|
|
691
|
+
// Shorthand / non-literal values cannot be reasoned about statically.
|
|
692
|
+
if ((asarProp != null && !ts.isPropertyAssignment(asarProp)) || (rootAsarProp != null && !ts.isPropertyAssignment(rootAsarProp))) {
|
|
693
|
+
if (unpackProp != null) {
|
|
694
|
+
manual("the platform or root asar value is not a literal.");
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (ownIsNull) {
|
|
699
|
+
if (unpackProp == null) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (rootAsarVal != null && rootAsarVal.kind === ts.SyntaxKind.FalseKeyword) {
|
|
703
|
+
this.removeProp(unpackProp);
|
|
704
|
+
this.changes.push({ key: `${name}.asarUnpack`, description: `removed ${name}.asarUnpack (no effect with asar: false)` });
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const rootAsarIsTrivial = rootAsarVal == null ||
|
|
708
|
+
rootAsarVal.kind === ts.SyntaxKind.TrueKeyword ||
|
|
709
|
+
rootAsarVal.kind === ts.SyntaxKind.NullKeyword ||
|
|
710
|
+
(ts.isObjectLiteralExpression(rootAsarVal) && rootAsarVal.properties.length === 0);
|
|
711
|
+
if (rootHasFlat || !rootAsarIsTrivial) {
|
|
712
|
+
manual(`${name}.asarUnpack is set alongside root-level asar options.`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (asarProp != null) {
|
|
716
|
+
this.removeProp(asarProp);
|
|
717
|
+
}
|
|
718
|
+
this.moveInto(platform, "asar", [{ prop: unpackProp, renameTo: "unpack" }], [], /* treatNullAsAbsent */ true);
|
|
719
|
+
this.changes.push({ key: `${name}.asarUnpack`, description: `moved ${name}.asarUnpack → ${name}.asar.unpack` });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (asarVal.kind === ts.SyntaxKind.FalseKeyword) {
|
|
723
|
+
if (unpackProp != null) {
|
|
724
|
+
this.removeProp(unpackProp);
|
|
725
|
+
this.changes.push({ key: `${name}.asarUnpack`, description: `removed ${name}.asarUnpack (no effect with ${name}.asar: false)` });
|
|
726
|
+
}
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
// `asar: true` or an object literal
|
|
730
|
+
if (rootHasFlat) {
|
|
731
|
+
manual("root-level asarUnpack / disableSanityCheckAsar / disableAsarIntegrity applied to this platform in v26.");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (unpackProp == null) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (asarVal.kind === ts.SyntaxKind.TrueKeyword) {
|
|
738
|
+
const braceIndent = this.lineIndentAt(this.start(asarProp.initializer));
|
|
739
|
+
this.replaceValue(asarProp.initializer, this.objectLiteralTextAt([this.entryText(unpackProp, braceIndent + this.indentUnit, "unpack")], braceIndent));
|
|
740
|
+
this.removeProp(unpackProp);
|
|
741
|
+
}
|
|
742
|
+
else if (ts.isObjectLiteralExpression(asarVal) && this.getProp(asarVal, "unpack") == null) {
|
|
743
|
+
this.moveInto(platform, "asar", [{ prop: unpackProp, renameTo: "unpack" }], []);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
manual(`${name}.asar is not a plain object literal, or already sets unpack.`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.changes.push({ key: `${name}.asarUnpack`, description: `moved ${name}.asarUnpack → ${name}.asar.unpack` });
|
|
750
|
+
}
|
|
751
|
+
ruleAppImageSystemIntegration(root) {
|
|
752
|
+
const appImage = this.getObjectProp(root, "appImage");
|
|
753
|
+
if (appImage == null) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const si = this.getProp(appImage, "systemIntegration");
|
|
757
|
+
if (si == null) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const appImageProp = this.getProp(root, "appImage");
|
|
761
|
+
if (appImage.properties.length === 1) {
|
|
762
|
+
this.removeProp(appImageProp);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
this.removeProp(si);
|
|
766
|
+
}
|
|
767
|
+
this.changes.push({ key: "appImage.systemIntegration", description: "removed appImage.systemIntegration (handled automatically by AppImageLauncher in v27)" });
|
|
768
|
+
}
|
|
769
|
+
rulePublish(parent, key, prefix = "") {
|
|
770
|
+
const ts = this.ts;
|
|
771
|
+
const prop = this.getProp(parent, key);
|
|
772
|
+
if (prop == null || !ts.isPropertyAssignment(prop)) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const value = this.unwrap(prop.initializer);
|
|
776
|
+
const entries = ts.isArrayLiteralExpression(value) ? value.elements : [value];
|
|
777
|
+
let changed = false;
|
|
778
|
+
for (const entry of entries) {
|
|
779
|
+
const obj = this.unwrap(entry);
|
|
780
|
+
if (!ts.isObjectLiteralExpression(obj)) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const provider = this.stringLiteralValue(this.getProp(obj, "provider"));
|
|
784
|
+
// GitLab keeps vPrefixedTagName in v27 (no tagNamePrefix equivalent) — leave it untouched.
|
|
785
|
+
if (provider !== "github") {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const vPrefixed = this.getProp(obj, "vPrefixedTagName");
|
|
789
|
+
const tagNamePrefix = this.getProp(obj, "tagNamePrefix");
|
|
790
|
+
const prefixText = this.stringLiteralValue(tagNamePrefix);
|
|
791
|
+
if (vPrefixed != null && ts.isPropertyAssignment(vPrefixed)) {
|
|
792
|
+
// v26 githubTagPrefix(): a non-empty tagNamePrefix won; otherwise vPrefixedTagName (default true) chose "v" or "".
|
|
793
|
+
const isFalse = vPrefixed.initializer.kind === ts.SyntaxKind.FalseKeyword;
|
|
794
|
+
if (tagNamePrefix == null) {
|
|
795
|
+
this.replaceRange(this.start(vPrefixed), vPrefixed.end, `tagNamePrefix: ${isFalse ? '""' : '"v"'}`);
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
if (prefixText === "") {
|
|
799
|
+
this.replaceValue(tagNamePrefix.initializer, isFalse ? '""' : '"v"');
|
|
800
|
+
}
|
|
801
|
+
this.removeProp(vPrefixed);
|
|
802
|
+
}
|
|
803
|
+
changed = true;
|
|
804
|
+
}
|
|
805
|
+
else if (prefixText === "") {
|
|
806
|
+
// v26 ignored an empty tagNamePrefix and still tagged "v1.2.3"; v27 honors it and would tag "1.2.3".
|
|
807
|
+
this.replaceValue(tagNamePrefix.initializer, '"v"');
|
|
808
|
+
changed = true;
|
|
809
|
+
this.warnings.push('A GitHub publish entry set tagNamePrefix: "". v26 ignored an empty prefix and tagged releases "v<version>"; v27 honors it. ' +
|
|
810
|
+
'It was rewritten to tagNamePrefix: "v" to keep your existing tag names — set it back to "" if you want unprefixed tags from now on.');
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (changed) {
|
|
814
|
+
this.changes.push({
|
|
815
|
+
key: `${prefix}${key}[].vPrefixedTagName`,
|
|
816
|
+
description: "replaced vPrefixedTagName with tagNamePrefix on GitHub publish entries",
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
ruleSnap(root) {
|
|
821
|
+
const ts = this.ts;
|
|
822
|
+
const snapProp = this.getProp(root, "snap");
|
|
823
|
+
if (snapProp == null || !ts.isPropertyAssignment(snapProp)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const snap = this.unwrap(snapProp.initializer);
|
|
827
|
+
if (!ts.isObjectLiteralExpression(snap)) {
|
|
828
|
+
this.removeProp(snapProp);
|
|
829
|
+
this.changes.push({ key: "snap", description: "removed empty snap config (use snapcraft in v27)" });
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
// snap's properties are re-emitted verbatim below, so rulePublish skipped it — flag what it would have rewritten.
|
|
833
|
+
const snapPublish = this.getProp(snap, "publish");
|
|
834
|
+
if (snapPublish != null && /\bvPrefixedTagName\b|\btagNamePrefix\s*:\s*(""|'')/.test(this.text.slice(this.start(snapPublish), snapPublish.end))) {
|
|
835
|
+
this.warnings.push("snap.publish sets vPrefixedTagName or an empty tagNamePrefix; after the move, apply the GitHub tagNamePrefix change under snapcraft.<base>.publish manually.");
|
|
836
|
+
}
|
|
837
|
+
const baseProp = this.getProp(snap, "base");
|
|
838
|
+
let base = this.stringLiteralValue(baseProp);
|
|
839
|
+
const restProps = snap.properties.filter((p) => p !== baseProp);
|
|
840
|
+
if (base === "custom") {
|
|
841
|
+
const childIndent = this.propIndentFor(root) + this.indentUnit;
|
|
842
|
+
const entries = ['base: "custom"', ...restProps.map((p) => this.entryText(p, childIndent))];
|
|
843
|
+
this.removeProp(snapProp);
|
|
844
|
+
this.createChild(root, "snapcraft", entries);
|
|
845
|
+
this.changes.push({ key: "snap", description: "moved snap (base: custom) → snapcraft verbatim" });
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
let assumed = false;
|
|
849
|
+
if (base == null || !SNAP_BASES.has(base)) {
|
|
850
|
+
if (base != null) {
|
|
851
|
+
this.warnings.push(`snap config had an unrecognized base "${base}"; assumed "core20". Verify the snapcraft.base value (core18/core20/core22/core24/custom).`);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
this.warnings.push('snap config had no "base"; assumed "core20" for snapcraft. Verify and adjust the base if needed (core18/core20/core22/core24).');
|
|
855
|
+
}
|
|
856
|
+
base = "core20";
|
|
857
|
+
assumed = true;
|
|
858
|
+
}
|
|
859
|
+
let nestedProps = restProps;
|
|
860
|
+
if (base === "core24") {
|
|
861
|
+
const unsupported = SNAP_CORE24_UNSUPPORTED.filter(k => this.getProp(snap, k) != null);
|
|
862
|
+
nestedProps = restProps.filter((p) => !unsupported.includes(this.propName(p)));
|
|
863
|
+
if (unsupported.length > 0) {
|
|
864
|
+
this.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.`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const baseChildIndent = this.propIndentFor(root) + this.indentUnit + this.indentUnit;
|
|
868
|
+
const nestedEntries = nestedProps.map((p) => this.entryText(p, baseChildIndent));
|
|
869
|
+
const snapcraftChildIndent = this.propIndentFor(root) + this.indentUnit;
|
|
870
|
+
const nestedObject = nestedEntries.length > 0 ? this.objectLiteralTextAt(nestedEntries, snapcraftChildIndent) : "{}";
|
|
871
|
+
const entries = [`base: "${base}"`, `${base}: ${nestedObject}`];
|
|
872
|
+
this.removeProp(snapProp);
|
|
873
|
+
this.createChild(root, "snapcraft", entries);
|
|
874
|
+
this.changes.push({ key: "snap", description: `moved snap → snapcraft.${base}${assumed ? " (base defaulted to core20)" : ""}` });
|
|
875
|
+
}
|
|
876
|
+
ruleHelperBundleId(root) {
|
|
877
|
+
const prop = this.getProp(root, "helper-bundle-id");
|
|
878
|
+
if (prop == null || !this.ts.isPropertyAssignment(prop)) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const mac = this.getObjectProp(root, "mac");
|
|
882
|
+
if (mac != null && this.getProp(mac, "helperBundleId") != null) {
|
|
883
|
+
this.removeProp(prop);
|
|
884
|
+
this.changes.push({ key: "helper-bundle-id", description: "moved helper-bundle-id → mac.helperBundleId" });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const ok = this.moveInto(root, "mac", [{ prop, renameTo: "helperBundleId" }], []);
|
|
888
|
+
if (ok) {
|
|
889
|
+
this.changes.push({ key: "helper-bundle-id", description: "moved helper-bundle-id → mac.helperBundleId" });
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
this.warnings.push("mac already exists but is not an object literal; move helper-bundle-id → mac.helperBundleId manually.");
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
ruleSquirrelNoMsi(root) {
|
|
896
|
+
const ts = this.ts;
|
|
897
|
+
const sq = this.getObjectProp(root, "squirrelWindows");
|
|
898
|
+
if (sq == null) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
// Not auto-migrated (different bundle layout) — left in place so the build fails with a targeted message.
|
|
902
|
+
if (this.getProp(sq, "customSquirrelVendorDir") != null) {
|
|
903
|
+
this.warnings.push("squirrelWindows.customSquirrelVendorDir was removed in v27 and cannot be migrated automatically. " +
|
|
904
|
+
'Supply the custom bundle via toolsets.squirrel: { url: "file:///abs/path" } — the bundle must contain an electron-winstaller/vendor/ subtree ' +
|
|
905
|
+
"(customSquirrelVendorDir pointed at the vendor files directly) — or remove the key to use the default Squirrel bundle.");
|
|
906
|
+
}
|
|
907
|
+
const noMsi = this.getProp(sq, "noMsi");
|
|
908
|
+
if (noMsi == null || !ts.isPropertyAssignment(noMsi)) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (this.getProp(sq, "msi") != null) {
|
|
912
|
+
this.removeProp(noMsi);
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
this.replaceRange(this.start(noMsi), noMsi.end, `msi: ${this.negate(noMsi.initializer)}`);
|
|
916
|
+
}
|
|
917
|
+
this.changes.push({ key: "squirrelWindows.noMsi", description: "replaced squirrelWindows.noMsi → squirrelWindows.msi (inverted boolean)" });
|
|
918
|
+
}
|
|
919
|
+
// Mirrors migrateConfig's migrateCustomNsis: customNsisBinary.debugLogging → <section>.installerDebugLogging (dropped for portable),
|
|
920
|
+
// an emptied or null customNsisBinary / customNsisResources is removed, and a custom bundle is kept and reported, never converted
|
|
921
|
+
// to toolsets.nsis (the bundle layout differs: it must carry the NSIS plugins).
|
|
922
|
+
ruleCustomNsis(root) {
|
|
923
|
+
const ts = this.ts;
|
|
924
|
+
const isNullLiteral = (prop) => ts.isPropertyAssignment(prop) && this.unwrap(prop.initializer).kind === ts.SyntaxKind.NullKeyword;
|
|
925
|
+
for (const section of NSIS_CONFIG_KEYS) {
|
|
926
|
+
const options = this.getObjectProp(root, section);
|
|
927
|
+
if (options == null) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const binaryPath = `${section}.customNsisBinary`;
|
|
931
|
+
const binaryProp = this.getProp(options, "customNsisBinary");
|
|
932
|
+
const binary = this.getObjectProp(options, "customNsisBinary");
|
|
933
|
+
if (binaryProp != null && isNullLiteral(binaryProp)) {
|
|
934
|
+
this.removeProp(binaryProp);
|
|
935
|
+
this.changes.push({ key: binaryPath, description: `removed ${binaryPath}: null (the key was removed in v27)` });
|
|
936
|
+
}
|
|
937
|
+
else if (binary != null) {
|
|
938
|
+
const found = this.getProp(binary, "debugLogging");
|
|
939
|
+
// A method / accessor named debugLogging is not read; it stays and keeps the key.
|
|
940
|
+
const debugProp = found != null && (ts.isPropertyAssignment(found) || ts.isShorthandPropertyAssignment(found)) ? found : null;
|
|
941
|
+
// A shorthand, spread, method or computed key may hold a custom bundle, so only `name: null` counts as unset.
|
|
942
|
+
const keepsBundle = binary.properties.some((p) => p !== debugProp && !isNullLiteral(p));
|
|
943
|
+
if (debugProp != null) {
|
|
944
|
+
const valueText = ts.isShorthandPropertyAssignment(debugProp) ? debugProp.name.text : this.valueText(debugProp.initializer, this.propIndentFor(options));
|
|
945
|
+
if (section === "portable") {
|
|
946
|
+
if (!isNullLiteral(debugProp)) {
|
|
947
|
+
this.warnings.push(PORTABLE_DEBUG_LOGGING_DROPPED);
|
|
948
|
+
}
|
|
949
|
+
this.changes.push({ key: `${binaryPath}.debugLogging`, description: `removed ${binaryPath}.debugLogging (no effect on portable targets)` });
|
|
950
|
+
}
|
|
951
|
+
else if (!isNullLiteral(debugProp) && this.getProp(options, "installerDebugLogging") == null) {
|
|
952
|
+
this.insertIntoObject(options, [`installerDebugLogging: ${valueText}`]);
|
|
953
|
+
this.changes.push({ key: `${binaryPath}.debugLogging`, description: `moved ${binaryPath}.debugLogging → ${section}.installerDebugLogging` });
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
this.changes.push({ key: `${binaryPath}.debugLogging`, description: `removed ${binaryPath}.debugLogging` });
|
|
957
|
+
}
|
|
958
|
+
if (keepsBundle) {
|
|
959
|
+
this.removeProp(debugProp);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (keepsBundle) {
|
|
963
|
+
this.warnings.push(legacyKeyMessage(binaryPath));
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
this.removeProp(binaryProp);
|
|
967
|
+
this.changes.push({ key: binaryPath, description: `removed ${binaryPath} (no custom bundle was set)` });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else if (binaryProp != null) {
|
|
971
|
+
this.warnings.push(legacyKeyMessage(binaryPath));
|
|
972
|
+
}
|
|
973
|
+
const resourcesPath = `${section}.customNsisResources`;
|
|
974
|
+
const resourcesProp = this.getProp(options, "customNsisResources");
|
|
975
|
+
if (resourcesProp != null && isNullLiteral(resourcesProp)) {
|
|
976
|
+
this.removeProp(resourcesProp);
|
|
977
|
+
this.changes.push({ key: resourcesPath, description: `removed ${resourcesPath}: null (the key was removed in v27)` });
|
|
978
|
+
}
|
|
979
|
+
else if (resourcesProp != null) {
|
|
980
|
+
this.warnings.push(legacyKeyMessage(resourcesPath));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
ruleWinSign(root) {
|
|
985
|
+
const ts = this.ts;
|
|
986
|
+
const win = this.getObjectProp(root, "win");
|
|
987
|
+
if (win == null) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const signNode = this.getProp(win, "sign");
|
|
991
|
+
const existingSignIsNull = signNode != null && ts.isPropertyAssignment(signNode) && this.unwrap(signNode.initializer).kind === ts.SyntaxKind.NullKeyword;
|
|
992
|
+
// `sign: null` is "not configured" in both versions — it neither blocks nor survives a migration below.
|
|
993
|
+
let signSet = signNode != null && !existingSignIsNull;
|
|
994
|
+
// Either flag set to false skipped ALL signing in v26, so both map to win.sign: false (signAndEditExecutable
|
|
995
|
+
// also skipped resource editing, which always runs in v27 — that part has no equivalent).
|
|
996
|
+
const isFalseProp = (prop) => prop != null && ts.isPropertyAssignment(prop) && prop.initializer.kind === ts.SyntaxKind.FalseKeyword;
|
|
997
|
+
const signAndEdit = this.getProp(win, "signAndEditExecutable");
|
|
998
|
+
const signExe = this.getProp(win, "signExecutable");
|
|
999
|
+
const signingDisabled = isFalseProp(signAndEdit) || isFalseProp(signExe);
|
|
1000
|
+
if (signAndEdit != null && ts.isPropertyAssignment(signAndEdit)) {
|
|
1001
|
+
this.removeProp(signAndEdit);
|
|
1002
|
+
if (isFalseProp(signAndEdit)) {
|
|
1003
|
+
this.warnings.push("win.signAndEditExecutable: false was used to skip both resource editing and signing. In v27, resource editing always runs; signing is still skipped via win.sign: false. There is no v27 equivalent that also skips resource editing — apply resources manually if needed.");
|
|
1004
|
+
}
|
|
1005
|
+
this.changes.push({ key: "win.signAndEditExecutable", description: "removed win.signAndEditExecutable (resource editing always runs in v27)" });
|
|
1006
|
+
}
|
|
1007
|
+
if (signExe != null && ts.isPropertyAssignment(signExe)) {
|
|
1008
|
+
this.removeProp(signExe);
|
|
1009
|
+
this.changes.push({ key: "win.signExecutable", description: "removed win.signExecutable (signing is enabled by default when credentials are available)" });
|
|
1010
|
+
}
|
|
1011
|
+
let signIsFalse = isFalseProp(signNode);
|
|
1012
|
+
if (signingDisabled && !signSet) {
|
|
1013
|
+
if (existingSignIsNull) {
|
|
1014
|
+
this.removeProp(signNode);
|
|
1015
|
+
}
|
|
1016
|
+
this.createChild(win, "sign", [], "false");
|
|
1017
|
+
signSet = true;
|
|
1018
|
+
signIsFalse = true;
|
|
1019
|
+
this.changes.push({ key: "win.sign", description: "set win.sign: false (v26 signExecutable/signAndEditExecutable: false disabled signing; resource editing still runs)" });
|
|
1020
|
+
}
|
|
1021
|
+
if (signIsFalse) {
|
|
1022
|
+
// Signing is switched off, so the legacy options never applied. Only key names are reported — values can hold passwords.
|
|
1023
|
+
const dropped = ["signtoolOptions", "azureSignOptions"].filter(k => this.getProp(win, k) != null);
|
|
1024
|
+
if (dropped.length > 0) {
|
|
1025
|
+
for (const k of dropped) {
|
|
1026
|
+
this.removeProp(this.getProp(win, k));
|
|
1027
|
+
this.changes.push({ key: `win.${k}`, description: `removed win.${k} (signing is disabled with win.sign: false)` });
|
|
1028
|
+
}
|
|
1029
|
+
this.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. ` +
|
|
1030
|
+
'To sign in v27, replace win.sign: false with win.sign: { type: "signtool" | "azure", … } carrying those options.');
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const azure = this.getObjectProp(win, "azureSignOptions");
|
|
1035
|
+
const signtool = this.getObjectProp(win, "signtoolOptions");
|
|
1036
|
+
const hasAzure = azure != null;
|
|
1037
|
+
const hasSigntool = signtool != null;
|
|
1038
|
+
if (!hasAzure && !hasSigntool) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (signSet) {
|
|
1042
|
+
this.warnings.push(`win.sign is already set alongside ${hasAzure ? "win.azureSignOptions" : "win.signtoolOptions"}. Remove the legacy key manually after verifying win.sign is correct.`);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (existingSignIsNull) {
|
|
1046
|
+
this.removeProp(signNode);
|
|
1047
|
+
}
|
|
1048
|
+
if (hasAzure && hasSigntool) {
|
|
1049
|
+
this.warnings.push("Both win.azureSignOptions and win.signtoolOptions are set. win.signtoolOptions will be dropped and win.azureSignOptions will be migrated to win.sign: { type: 'azure', … } (Azure took priority in v26). Verify the migrated win.sign block is correct for your project.");
|
|
1050
|
+
this.removeProp(this.getProp(win, "signtoolOptions"));
|
|
1051
|
+
}
|
|
1052
|
+
if (hasAzure) {
|
|
1053
|
+
this.buildWinAzureSign(win, azure);
|
|
1054
|
+
this.removeProp(this.getProp(win, "azureSignOptions"));
|
|
1055
|
+
this.changes.push({ key: "win.azureSignOptions", description: 'moved win.azureSignOptions → win.sign: { type: "azure", … }' });
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
this.buildWinSigntoolSign(win, signtool);
|
|
1059
|
+
this.removeProp(this.getProp(win, "signtoolOptions"));
|
|
1060
|
+
this.changes.push({ key: "win.signtoolOptions", description: 'moved win.signtoolOptions → win.sign: { type: "signtool", … }' });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
buildWinSigntoolSign(win, signtool) {
|
|
1064
|
+
const ts = this.ts;
|
|
1065
|
+
const childIndent = this.propIndentFor(win) + this.indentUnit;
|
|
1066
|
+
const entries = [];
|
|
1067
|
+
for (const p of signtool.properties) {
|
|
1068
|
+
if (ts.isPropertyAssignment(p) && this.propName(p) === "type") {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
entries.push(this.entryText(p, childIndent));
|
|
1072
|
+
}
|
|
1073
|
+
entries.push('type: "signtool"');
|
|
1074
|
+
this.createChild(win, "sign", entries);
|
|
1075
|
+
}
|
|
1076
|
+
buildWinAzureSign(win, azure) {
|
|
1077
|
+
const ts = this.ts;
|
|
1078
|
+
const childIndent = this.propIndentFor(win) + this.indentUnit;
|
|
1079
|
+
const metaIndent = childIndent + this.indentUnit;
|
|
1080
|
+
const knownEntries = [];
|
|
1081
|
+
const metaEntries = [];
|
|
1082
|
+
const extraKeys = [];
|
|
1083
|
+
let existingMeta = null;
|
|
1084
|
+
for (const p of azure.properties) {
|
|
1085
|
+
if (!ts.isPropertyAssignment(p)) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const name = this.propName(p);
|
|
1089
|
+
if (name === "type") {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (name === "additionalMetadata") {
|
|
1093
|
+
existingMeta = this.unwrap(p.initializer);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
const isString = ts.isStringLiteral(p.initializer);
|
|
1097
|
+
if (AZURE_KNOWN_FIELDS.has(name)) {
|
|
1098
|
+
knownEntries.push(this.entryText(p, childIndent));
|
|
1099
|
+
}
|
|
1100
|
+
else if (isString) {
|
|
1101
|
+
metaEntries.push(this.entryText(p, metaIndent));
|
|
1102
|
+
extraKeys.push(name);
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
// Unknown non-string field — keep verbatim so nothing is silently dropped.
|
|
1106
|
+
knownEntries.push(this.entryText(p, childIndent));
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (existingMeta != null && ts.isObjectLiteralExpression(existingMeta)) {
|
|
1110
|
+
for (const p of existingMeta.properties) {
|
|
1111
|
+
metaEntries.push(this.entryText(p, metaIndent));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const signEntries = [...knownEntries];
|
|
1115
|
+
if (metaEntries.length > 0) {
|
|
1116
|
+
signEntries.push(`additionalMetadata: ${this.objectLiteralTextAt(metaEntries, childIndent)}`);
|
|
1117
|
+
this.changes.push({ key: "win.azureSignOptions", description: `moved extra keys [${extraKeys.join(", ")}] into win.sign.additionalMetadata` });
|
|
1118
|
+
}
|
|
1119
|
+
signEntries.push('type: "azure"');
|
|
1120
|
+
this.createChild(win, "sign", signEntries);
|
|
1121
|
+
}
|
|
1122
|
+
ruleMacSigning(platform, name) {
|
|
1123
|
+
const ts = this.ts;
|
|
1124
|
+
// Removed outright, not moved: ElectronSignOptions omits these and the schema rejects them.
|
|
1125
|
+
for (const field of MAC_SIGN_REMOVED_FIELDS) {
|
|
1126
|
+
const prop = this.getProp(platform, field);
|
|
1127
|
+
if (prop != null) {
|
|
1128
|
+
this.removeProp(prop);
|
|
1129
|
+
this.changes.push({
|
|
1130
|
+
key: `${name}.${field}`,
|
|
1131
|
+
description: `removed ${name}.${field} (@electron/osx-sign 2.x dropped the spctl --assess step; there is no ${name}.sign.${field})`,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const present = MAC_SIGN_FIELDS.filter(f => this.getProp(platform, f) != null);
|
|
1136
|
+
const signIgnore = this.getProp(platform, "signIgnore");
|
|
1137
|
+
const hasSignIgnore = signIgnore != null;
|
|
1138
|
+
const existingSignProp = this.getProp(platform, "sign");
|
|
1139
|
+
const existingSign = existingSignProp != null && ts.isPropertyAssignment(existingSignProp) ? this.unwrap(existingSignProp.initializer) : null;
|
|
1140
|
+
const signIsCustom = existingSign != null && (ts.isStringLiteral(existingSign) || ts.isArrowFunction(existingSign) || ts.isFunctionExpression(existingSign) || ts.isIdentifier(existingSign));
|
|
1141
|
+
const signIsNull = existingSign != null && existingSign.kind === ts.SyntaxKind.NullKeyword;
|
|
1142
|
+
if (present.length === 0 && !hasSignIgnore) {
|
|
1143
|
+
if (signIsNull) {
|
|
1144
|
+
this.removeProp(existingSignProp);
|
|
1145
|
+
this.changes.push({ key: `${name}.sign`, description: `removed ${name}.sign: null (v26 "no custom signer" = v27 default; sign: null now means skip signing)` });
|
|
1146
|
+
}
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (signIsCustom) {
|
|
1150
|
+
const fields = [...present, ...(hasSignIgnore ? ["signIgnore"] : [])].join(", ");
|
|
1151
|
+
this.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.`);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const sources = [];
|
|
1155
|
+
for (const f of present) {
|
|
1156
|
+
const prop = this.getProp(platform, f);
|
|
1157
|
+
if (this.dropNullField(prop, f, name)) {
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
sources.push({ prop });
|
|
1161
|
+
this.changes.push({ key: `${name}.${f}`, description: `moved ${name}.${f} → ${name}.sign.${f}` });
|
|
1162
|
+
}
|
|
1163
|
+
if (hasSignIgnore && !this.dropNullField(signIgnore, "signIgnore", name)) {
|
|
1164
|
+
sources.push({ prop: signIgnore, renameTo: "ignore" });
|
|
1165
|
+
this.changes.push({ key: `${name}.signIgnore`, description: `renamed ${name}.signIgnore → ${name}.sign.ignore` });
|
|
1166
|
+
}
|
|
1167
|
+
// A bare `sign: null` should be dropped before we build the options object.
|
|
1168
|
+
if (signIsNull) {
|
|
1169
|
+
this.removeProp(existingSignProp);
|
|
1170
|
+
if (sources.length === 0) {
|
|
1171
|
+
this.changes.push({ key: `${name}.sign`, description: `removed ${name}.sign: null (v26 "no custom signer" = v27 default; sign: null now means skip signing)` });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (sources.length > 0) {
|
|
1175
|
+
this.moveInto(platform, "sign", sources, [], /* treatNullAsAbsent */ true);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/** Drops a v26 `null` ("unset") for a field whose v27 type has no null branch. Returns true when dropped. */
|
|
1179
|
+
dropNullField(prop, field, name) {
|
|
1180
|
+
const ts = this.ts;
|
|
1181
|
+
if (!MAC_NULL_MEANS_UNSET_FIELDS.has(field) || !ts.isPropertyAssignment(prop) || this.unwrap(prop.initializer).kind !== ts.SyntaxKind.NullKeyword) {
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
this.removeProp(prop);
|
|
1185
|
+
this.changes.push({ key: `${name}.${field}`, description: `removed ${name}.${field}: null (null meant "unset" in v26; the v27 option does not accept null)` });
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
ruleMacUniversal(platform, name) {
|
|
1189
|
+
const present = MAC_UNIVERSAL_FIELDS.filter(f => this.getProp(platform, f) != null);
|
|
1190
|
+
if (present.length === 0) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const sources = [];
|
|
1194
|
+
for (const f of present) {
|
|
1195
|
+
const prop = this.getProp(platform, f);
|
|
1196
|
+
if (!this.dropNullField(prop, f, name)) {
|
|
1197
|
+
this.changes.push({ key: `${name}.${f}`, description: `moved ${name}.${f} → ${name}.universal.${f}` });
|
|
1198
|
+
sources.push({ prop });
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (sources.length === 0) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const ok = this.moveInto(platform, "universal", sources, []);
|
|
1205
|
+
if (!ok) {
|
|
1206
|
+
this.warnings.push(`${name}.universal already exists but is not an object literal; move ${present.join(", ")} into it manually.`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
ruleElectronDownload(root) {
|
|
1210
|
+
const ts = this.ts;
|
|
1211
|
+
let sourceKey = "electronDownload";
|
|
1212
|
+
let prop = this.getProp(root, "electronDownload");
|
|
1213
|
+
if (prop == null) {
|
|
1214
|
+
// A hand-renamed `electronGet` that still carries the v26 shape.
|
|
1215
|
+
const electronGet = this.getObjectProp(root, "electronGet");
|
|
1216
|
+
if (electronGet == null || !electronGet.properties.some((p) => isLegacyElectronDownloadKey(this.propName(p) ?? ""))) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
sourceKey = "electronGet";
|
|
1220
|
+
prop = this.getProp(root, "electronGet");
|
|
1221
|
+
}
|
|
1222
|
+
else if (this.getProp(root, "electronGet") != null) {
|
|
1223
|
+
this.warnings.push("Both electronDownload and electronGet are set; merge electronDownload into electronGet manually (mirror → mirrorOptions.mirror, isVerifyChecksum: false → unsafelyDisableChecksums: true).");
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const old = this.unwrap(prop.initializer);
|
|
1230
|
+
if (!ts.isObjectLiteralExpression(old)) {
|
|
1231
|
+
this.renameKey(prop, "electronGet");
|
|
1232
|
+
this.changes.push({ key: "electronDownload", description: "renamed electronDownload → electronGet" });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const childIndent = this.propIndentFor(root) + this.indentUnit;
|
|
1236
|
+
const entries = [];
|
|
1237
|
+
const dropped = [];
|
|
1238
|
+
let mirrorValueText = null;
|
|
1239
|
+
let existingMirrorOptions = null;
|
|
1240
|
+
for (const p of old.properties) {
|
|
1241
|
+
if (!ts.isPropertyAssignment(p)) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
const name = this.propName(p);
|
|
1245
|
+
if (name === "mirror") {
|
|
1246
|
+
mirrorValueText = this.valueText(p.initializer, childIndent + this.indentUnit);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (name === "isVerifyChecksum") {
|
|
1250
|
+
if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1251
|
+
entries.push("unsafelyDisableChecksums: true");
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
this.warnings.push("electronGet: isVerifyChecksum had a non-false value; review electronGet.unsafelyDisableChecksums manually.");
|
|
1255
|
+
}
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (ELECTRON_DOWNLOAD_DROPPED.includes(name)) {
|
|
1259
|
+
dropped.push(name);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (name === "mirrorOptions") {
|
|
1263
|
+
existingMirrorOptions = this.unwrap(p.initializer);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
entries.push(this.entryText(p, childIndent));
|
|
1267
|
+
}
|
|
1268
|
+
if (mirrorValueText != null || existingMirrorOptions != null) {
|
|
1269
|
+
const moInner = [];
|
|
1270
|
+
if (existingMirrorOptions != null && ts.isObjectLiteralExpression(existingMirrorOptions)) {
|
|
1271
|
+
for (const p of existingMirrorOptions.properties) {
|
|
1272
|
+
moInner.push(this.entryText(p, childIndent + this.indentUnit));
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (mirrorValueText != null) {
|
|
1276
|
+
moInner.push(`mirror: ${mirrorValueText}`);
|
|
1277
|
+
}
|
|
1278
|
+
entries.push(`mirrorOptions: ${this.objectLiteralTextAt(moInner, childIndent)}`);
|
|
1279
|
+
}
|
|
1280
|
+
if (dropped.length > 0) {
|
|
1281
|
+
this.warnings.push(`electronGet (formerly electronDownload) dropped [${dropped.join(", ")}] — these have no equivalent in @electron/get v5. Set a mirror via electronGet.mirrorOptions if needed.`);
|
|
1282
|
+
}
|
|
1283
|
+
this.removeProp(prop);
|
|
1284
|
+
this.createChild(root, "electronGet", entries);
|
|
1285
|
+
this.changes.push({
|
|
1286
|
+
key: sourceKey,
|
|
1287
|
+
description: sourceKey === "electronDownload"
|
|
1288
|
+
? "renamed electronDownload → electronGet (mirror → mirrorOptions.mirror; isVerifyChecksum → unsafelyDisableChecksums)"
|
|
1289
|
+
: "reshaped electronGet to the v27 options (mirror → mirrorOptions.mirror; isVerifyChecksum → unsafelyDisableChecksums)",
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
// ── Edit primitives ───────────────────────────────────────────────────────
|
|
1293
|
+
/**
|
|
1294
|
+
* Moves `sources` properties into a child object `childKey` of `parent` (creating it when absent).
|
|
1295
|
+
* Returns false when the child exists but is not an object literal (caller should warn).
|
|
1296
|
+
*/
|
|
1297
|
+
moveInto(parent, childKey, sources, extraEntries, treatNullAsAbsent = false) {
|
|
1298
|
+
const ts = this.ts;
|
|
1299
|
+
const existingProp = this.getProp(parent, childKey);
|
|
1300
|
+
let existingObj = null;
|
|
1301
|
+
if (existingProp != null && ts.isPropertyAssignment(existingProp)) {
|
|
1302
|
+
const v = this.unwrap(existingProp.initializer);
|
|
1303
|
+
if (ts.isObjectLiteralExpression(v)) {
|
|
1304
|
+
existingObj = v;
|
|
1305
|
+
}
|
|
1306
|
+
else if (!(treatNullAsAbsent && v.kind === ts.SyntaxKind.NullKeyword)) {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (existingObj != null) {
|
|
1311
|
+
const childIndent = this.propIndentFor(existingObj);
|
|
1312
|
+
const entries = sources.map(s => this.entryText(s.prop, childIndent, s.renameTo)).concat(extraEntries);
|
|
1313
|
+
for (const s of sources) {
|
|
1314
|
+
this.removeProp(s.prop);
|
|
1315
|
+
}
|
|
1316
|
+
this.insertIntoObject(existingObj, entries);
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
const childIndent = this.propIndentFor(parent) + this.indentUnit;
|
|
1320
|
+
const entries = sources.map(s => this.entryText(s.prop, childIndent, s.renameTo)).concat(extraEntries);
|
|
1321
|
+
for (const s of sources) {
|
|
1322
|
+
this.removeProp(s.prop);
|
|
1323
|
+
}
|
|
1324
|
+
this.createChild(parent, childKey, entries);
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
/** Prepends `childKey: { entries }` (or `childKey: rawValue`) as the first property of `parent`. */
|
|
1328
|
+
createChild(parent, childKey, entries, rawValue) {
|
|
1329
|
+
const parentPropIndent = this.propIndentFor(parent);
|
|
1330
|
+
const value = rawValue != null ? rawValue : this.objectLiteralTextAt(entries, parentPropIndent);
|
|
1331
|
+
const braceEnd = this.start(parent) + 1;
|
|
1332
|
+
this.insertEdit(braceEnd, `\n${parentPropIndent}${childKey}: ${value},`);
|
|
1333
|
+
}
|
|
1334
|
+
/** Inserts already-built "key: value" entries as the first properties of `objLit`. */
|
|
1335
|
+
insertIntoObject(objLit, entries) {
|
|
1336
|
+
if (entries.length === 0) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const propIndent = this.propIndentFor(objLit);
|
|
1340
|
+
const braceEnd = this.start(objLit) + 1;
|
|
1341
|
+
const body = entries.map(e => `\n${propIndent}${e},`).join("");
|
|
1342
|
+
if (objLit.properties.length > 0) {
|
|
1343
|
+
this.insertEdit(braceEnd, body);
|
|
1344
|
+
}
|
|
1345
|
+
else {
|
|
1346
|
+
const objLineIndent = this.lineIndentAt(this.start(objLit));
|
|
1347
|
+
this.insertEdit(braceEnd, body + `\n${objLineIndent}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/** Renders a multi-line object literal whose closing brace aligns to `braceIndent`. */
|
|
1351
|
+
objectLiteralTextAt(entries, braceIndent) {
|
|
1352
|
+
if (entries.length === 0) {
|
|
1353
|
+
return "{}";
|
|
1354
|
+
}
|
|
1355
|
+
const propIndent = braceIndent + this.indentUnit;
|
|
1356
|
+
const body = entries.map(e => `\n${propIndent}${e},`).join("");
|
|
1357
|
+
return `{${body}\n${braceIndent}}`;
|
|
1358
|
+
}
|
|
1359
|
+
removeProp(prop) {
|
|
1360
|
+
if (prop == null) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
this.replaceRange(prop.pos, this.endWithTrailingComma(prop), "");
|
|
1364
|
+
}
|
|
1365
|
+
renameKey(prop, newName) {
|
|
1366
|
+
this.replaceRange(this.start(prop.name), prop.name.end, newName);
|
|
1367
|
+
}
|
|
1368
|
+
replaceValue(valueNode, newText) {
|
|
1369
|
+
this.replaceRange(this.start(valueNode), valueNode.end, newText);
|
|
1370
|
+
}
|
|
1371
|
+
/** "key: value" text for a property, re-indenting a multi-line value to `targetIndent`. */
|
|
1372
|
+
entryText(prop, targetIndent, renameTo) {
|
|
1373
|
+
if (prop.name == null) {
|
|
1374
|
+
// Spread assignment (`...x`) or other nameless member — capture verbatim, re-indented.
|
|
1375
|
+
return this.reindent(this.text.slice(this.start(prop), prop.end), this.lineIndentAt(this.start(prop)), targetIndent);
|
|
1376
|
+
}
|
|
1377
|
+
const keyText = renameTo != null ? renameTo : this.text.slice(this.start(prop.name), prop.name.end);
|
|
1378
|
+
if (this.ts.isPropertyAssignment(prop)) {
|
|
1379
|
+
return `${keyText}: ${this.valueText(prop.initializer, targetIndent)}`;
|
|
1380
|
+
}
|
|
1381
|
+
if (this.ts.isShorthandPropertyAssignment(prop)) {
|
|
1382
|
+
return keyText;
|
|
1383
|
+
}
|
|
1384
|
+
// Method / accessor — capture verbatim, re-indented.
|
|
1385
|
+
return this.reindent(this.text.slice(this.start(prop), prop.end), this.lineIndentAt(this.start(prop)), targetIndent);
|
|
1386
|
+
}
|
|
1387
|
+
/** Source text of a value node, re-indenting continuation lines from its original to `targetIndent`. */
|
|
1388
|
+
valueText(valueNode, targetIndent) {
|
|
1389
|
+
const raw = this.text.slice(this.start(valueNode), valueNode.end);
|
|
1390
|
+
if (!raw.includes("\n")) {
|
|
1391
|
+
return raw;
|
|
1392
|
+
}
|
|
1393
|
+
const fromIndent = this.lineIndentAt(this.start(valueNode));
|
|
1394
|
+
return this.reindent(raw, fromIndent, targetIndent);
|
|
1395
|
+
}
|
|
1396
|
+
mergeArrays(valueNodes) {
|
|
1397
|
+
const ts = this.ts;
|
|
1398
|
+
const elems = [];
|
|
1399
|
+
for (const v of valueNodes) {
|
|
1400
|
+
const node = this.unwrap(v);
|
|
1401
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1402
|
+
for (const el of node.elements) {
|
|
1403
|
+
elems.push(this.text.slice(this.start(el), el.end));
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
elems.push(this.text.slice(this.start(node), node.end));
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return `[${elems.join(", ")}]`;
|
|
1411
|
+
}
|
|
1412
|
+
negate(valueNode) {
|
|
1413
|
+
const ts = this.ts;
|
|
1414
|
+
if (valueNode.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1415
|
+
return "false";
|
|
1416
|
+
}
|
|
1417
|
+
if (valueNode.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1418
|
+
return "true";
|
|
1419
|
+
}
|
|
1420
|
+
return `!(${this.text.slice(this.start(valueNode), valueNode.end)})`;
|
|
1421
|
+
}
|
|
1422
|
+
// ── Low-level helpers ──────────────────────────────────────────────────────
|
|
1423
|
+
start(node) {
|
|
1424
|
+
return node.getStart(this.sf);
|
|
1425
|
+
}
|
|
1426
|
+
getProp(objLit, name) {
|
|
1427
|
+
const ts = this.ts;
|
|
1428
|
+
for (const p of objLit.properties) {
|
|
1429
|
+
if (p.name == null) {
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
let key;
|
|
1433
|
+
if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name) || ts.isNumericLiteral(p.name)) {
|
|
1434
|
+
key = p.name.text;
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (key === name) {
|
|
1440
|
+
return p;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
getObjectProp(objLit, name) {
|
|
1446
|
+
const ts = this.ts;
|
|
1447
|
+
const prop = this.getProp(objLit, name);
|
|
1448
|
+
if (prop == null || !ts.isPropertyAssignment(prop)) {
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
const v = this.unwrap(prop.initializer);
|
|
1452
|
+
return ts.isObjectLiteralExpression(v) ? v : null;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Value of property `name` for the advisory rules: undefined when `objLit` is null or lacks the property (or sets it to `undefined`,
|
|
1456
|
+
* which JSON drops), else the unwrapped initializer of a `name: value` property, or a node the AST cannot read: the property itself
|
|
1457
|
+
* for a shorthand / method, or a spread / computed key that may set the property (one after it, or anywhere when it is absent).
|
|
1458
|
+
*/
|
|
1459
|
+
propValue(objLit, name) {
|
|
1460
|
+
const ts = this.ts;
|
|
1461
|
+
if (objLit == null) {
|
|
1462
|
+
return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
const prop = this.getProp(objLit, name);
|
|
1465
|
+
const unread = objLit.properties
|
|
1466
|
+
.slice(prop == null ? 0 : objLit.properties.indexOf(prop) + 1)
|
|
1467
|
+
.find((p) => ts.isSpreadAssignment(p) || (p.name != null && ts.isComputedPropertyName(p.name)));
|
|
1468
|
+
if (unread != null || prop == null) {
|
|
1469
|
+
return unread;
|
|
1470
|
+
}
|
|
1471
|
+
const value = ts.isPropertyAssignment(prop) ? this.unwrap(prop.initializer) : prop;
|
|
1472
|
+
return ts.isIdentifier(value) && value.text === "undefined" ? undefined : value;
|
|
1473
|
+
}
|
|
1474
|
+
/** `node` when it is an object literal, else null. */
|
|
1475
|
+
objectLiteral(node) {
|
|
1476
|
+
return node != null && this.ts.isObjectLiteralExpression(node) ? node : null;
|
|
1477
|
+
}
|
|
1478
|
+
/** True for a value the AST reads like JSON: a string, number, boolean, null, or an object / array literal. */
|
|
1479
|
+
isLiteral(node) {
|
|
1480
|
+
const ts = this.ts;
|
|
1481
|
+
return (ts.isStringLiteralLike(node) ||
|
|
1482
|
+
ts.isNumericLiteral(node) ||
|
|
1483
|
+
ts.isObjectLiteralExpression(node) ||
|
|
1484
|
+
ts.isArrayLiteralExpression(node) ||
|
|
1485
|
+
[ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind));
|
|
1486
|
+
}
|
|
1487
|
+
propName(prop) {
|
|
1488
|
+
const ts = this.ts;
|
|
1489
|
+
if (prop.name != null && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) || ts.isNumericLiteral(prop.name))) {
|
|
1490
|
+
return prop.name.text;
|
|
1491
|
+
}
|
|
1492
|
+
return undefined;
|
|
1493
|
+
}
|
|
1494
|
+
stringLiteralValue(prop) {
|
|
1495
|
+
const ts = this.ts;
|
|
1496
|
+
if (prop == null || !ts.isPropertyAssignment(prop)) {
|
|
1497
|
+
return undefined;
|
|
1498
|
+
}
|
|
1499
|
+
const v = this.unwrap(prop.initializer);
|
|
1500
|
+
return ts.isStringLiteral(v) ? v.text : undefined;
|
|
1501
|
+
}
|
|
1502
|
+
endWithTrailingComma(prop) {
|
|
1503
|
+
let i = prop.end;
|
|
1504
|
+
while (i < this.text.length && (this.text[i] === " " || this.text[i] === "\t")) {
|
|
1505
|
+
i++;
|
|
1506
|
+
}
|
|
1507
|
+
return this.text[i] === "," ? i + 1 : prop.end;
|
|
1508
|
+
}
|
|
1509
|
+
lineIndentAt(pos) {
|
|
1510
|
+
let i = pos;
|
|
1511
|
+
while (i > 0 && this.text[i - 1] !== "\n") {
|
|
1512
|
+
i--;
|
|
1513
|
+
}
|
|
1514
|
+
let j = i;
|
|
1515
|
+
while (j < this.text.length && (this.text[j] === " " || this.text[j] === "\t")) {
|
|
1516
|
+
j++;
|
|
1517
|
+
}
|
|
1518
|
+
return this.text.slice(i, j);
|
|
1519
|
+
}
|
|
1520
|
+
detectIndentUnit(root) {
|
|
1521
|
+
const braceIndent = this.lineIndentAt(this.start(root));
|
|
1522
|
+
for (const p of root.properties) {
|
|
1523
|
+
const pi = this.lineIndentAt(this.start(p));
|
|
1524
|
+
if (pi.length > braceIndent.length) {
|
|
1525
|
+
return pi.slice(braceIndent.length);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return " ";
|
|
1529
|
+
}
|
|
1530
|
+
propIndentFor(objLit) {
|
|
1531
|
+
for (const p of objLit.properties) {
|
|
1532
|
+
return this.lineIndentAt(this.start(p));
|
|
1533
|
+
}
|
|
1534
|
+
return this.lineIndentAt(this.start(objLit)) + this.indentUnit;
|
|
1535
|
+
}
|
|
1536
|
+
reindent(text, fromIndent, toIndent) {
|
|
1537
|
+
const delta = toIndent.length - fromIndent.length;
|
|
1538
|
+
if (delta === 0) {
|
|
1539
|
+
return text;
|
|
1540
|
+
}
|
|
1541
|
+
const lines = text.split("\n");
|
|
1542
|
+
return lines
|
|
1543
|
+
.map((line, idx) => {
|
|
1544
|
+
if (idx === 0) {
|
|
1545
|
+
return line;
|
|
1546
|
+
}
|
|
1547
|
+
if (delta > 0) {
|
|
1548
|
+
return " ".repeat(delta) + line;
|
|
1549
|
+
}
|
|
1550
|
+
let removed = 0;
|
|
1551
|
+
let k = 0;
|
|
1552
|
+
while (k < line.length && removed < -delta && line[k] === " ") {
|
|
1553
|
+
k++;
|
|
1554
|
+
removed++;
|
|
1555
|
+
}
|
|
1556
|
+
return line.slice(k);
|
|
1557
|
+
})
|
|
1558
|
+
.join("\n");
|
|
1559
|
+
}
|
|
1560
|
+
replaceRange(start, end, text) {
|
|
1561
|
+
this.edits.push({ start, end, text });
|
|
1562
|
+
}
|
|
1563
|
+
insertEdit(pos, text) {
|
|
1564
|
+
this.edits.push({ start: pos, end: pos, text });
|
|
1565
|
+
}
|
|
1566
|
+
apply() {
|
|
1567
|
+
const edits = [...this.edits].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1568
|
+
let out = "";
|
|
1569
|
+
let cursor = 0;
|
|
1570
|
+
for (const e of edits) {
|
|
1571
|
+
if (e.start < cursor) {
|
|
1572
|
+
throw new Error(`overlapping edits at ${e.start} (cursor ${cursor})`);
|
|
1573
|
+
}
|
|
1574
|
+
out += this.text.slice(cursor, e.start) + e.text;
|
|
1575
|
+
cursor = e.end;
|
|
1576
|
+
}
|
|
1577
|
+
out += this.text.slice(cursor);
|
|
1578
|
+
return out;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
//# sourceMappingURL=migrate-schema-programmatic.js.map
|