@lvce-editor/update-worker 2.13.0 → 2.15.0

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/README.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  Webworker for the update functionality in LVCE Editor.
4
4
 
5
+ ## Emulating the current version
6
+
7
+ Set `update.emulateCurrentVersion` in LVCE Editor's `settings.json` to test update detection without installing an older build:
8
+
9
+ ```json
10
+ {
11
+ "update.emulateCurrentVersion": "0.115.14"
12
+ }
13
+ ```
14
+
15
+ Update checks compare the latest GitHub release with this version. Remove the setting to use the normal application version. Empty, non-string, or malformed values also fall back to the application version; overrides must use numeric `major.minor.patch` format. The override only affects update comparisons, not the application's reported version or the selected release asset.
16
+
17
+ The host must expose `Preferences.get` and `Process.getVersion` to the worker and use `Update.getLatestVersion` for manual update detection. Both manual checks and the install flow use the override. This requires an application build containing the worker and host integration; existing installed builds do not gain the setting automatically.
18
+
5
19
  ## Contributing
6
20
 
7
21
  ```sh
@@ -10,3 +24,9 @@ cd update-worker &&
10
24
  npm ci &&
11
25
  npm test
12
26
  ```
27
+
28
+ ## macOS updates
29
+
30
+ The worker selects the release DMG matching the app's architecture, downloads it, and verifies its size and GitHub SHA-256 digest before writing it through the existing filesystem RPC. It then asks the host's small native helper to stage and validate the bundle. Install Update and Restart are separate confirmations; cancelling Restart leaves the app running. The Windows NSIS path remains in place.
31
+
32
+ The host must expose `AutoUpdater.getPlatform`, `Process.getArch`, `AutoUpdater.stageMacUpdate`, and `AutoUpdater.restartMacUpdate`. Installation errors are returned to the host for notification. A release without the requested architecture or checksum is rejected. The native helper and host wiring must ship together with this worker before the installed macOS app can use the flow.
@@ -96,7 +96,6 @@ const walkValue = (value, transferrables, isTransferrable) => {
96
96
  for (const property of Object.values(value)) {
97
97
  walkValue(property, transferrables, isTransferrable);
98
98
  }
99
- return;
100
99
  }
101
100
  };
102
101
  const getTransferrables = value => {
@@ -250,7 +249,14 @@ class IpcError extends VError {
250
249
  const cause = new Error(message);
251
250
  // @ts-ignore
252
251
  cause.code = code;
253
- cause.stack = stack;
252
+ if (stack) {
253
+ Object.defineProperty(cause, 'stack', {
254
+ configurable: true,
255
+ enumerable: false,
256
+ value: stack,
257
+ writable: true
258
+ });
259
+ }
254
260
  super(cause, betterMessage);
255
261
  } else {
256
262
  super(betterMessage);
@@ -959,6 +965,34 @@ const listen$1 = async (module, options) => {
959
965
  return ipc;
960
966
  };
961
967
 
968
+ const createSharedLazyRpc = factory => {
969
+ let rpcPromise;
970
+ const getOrCreate = () => {
971
+ if (!rpcPromise) {
972
+ rpcPromise = factory();
973
+ }
974
+ return rpcPromise;
975
+ };
976
+ return {
977
+ async dispose() {
978
+ const rpc = await getOrCreate();
979
+ await rpc.dispose();
980
+ },
981
+ async invoke(method, ...params) {
982
+ const rpc = await getOrCreate();
983
+ return rpc.invoke(method, ...params);
984
+ },
985
+ async invokeAndTransfer(method, ...params) {
986
+ const rpc = await getOrCreate();
987
+ return rpc.invokeAndTransfer(method, ...params);
988
+ },
989
+ async send(method, ...params) {
990
+ const rpc = await getOrCreate();
991
+ rpc.send(method, ...params);
992
+ }
993
+ };
994
+ };
995
+
962
996
  const create$4 = async ({
963
997
  commandMap,
964
998
  isMessagePortOpen = true,
@@ -994,34 +1028,6 @@ const create$3 = async ({
994
1028
  });
995
1029
  };
996
1030
 
997
- const createSharedLazyRpc = factory => {
998
- let rpcPromise;
999
- const getOrCreate = () => {
1000
- if (!rpcPromise) {
1001
- rpcPromise = factory();
1002
- }
1003
- return rpcPromise;
1004
- };
1005
- return {
1006
- async dispose() {
1007
- const rpc = await getOrCreate();
1008
- await rpc.dispose();
1009
- },
1010
- async invoke(method, ...params) {
1011
- const rpc = await getOrCreate();
1012
- return rpc.invoke(method, ...params);
1013
- },
1014
- async invokeAndTransfer(method, ...params) {
1015
- const rpc = await getOrCreate();
1016
- return rpc.invokeAndTransfer(method, ...params);
1017
- },
1018
- async send(method, ...params) {
1019
- const rpc = await getOrCreate();
1020
- rpc.send(method, ...params);
1021
- }
1022
- };
1023
- };
1024
-
1025
1031
  const create$2 = async ({
1026
1032
  commandMap,
1027
1033
  isMessagePortOpen,
@@ -1115,9 +1121,14 @@ const create = rpcId => {
1115
1121
  };
1116
1122
  };
1117
1123
 
1124
+ const Electron = 2;
1125
+
1118
1126
  const DialogWorker = 7014;
1119
1127
  const RendererWorker = 1;
1120
1128
 
1129
+ const DownloadingUpdate = 2;
1130
+ const DownloadedUpdate = 3;
1131
+
1121
1132
  const {
1122
1133
  invoke: invoke$1,
1123
1134
  set: set$1
@@ -1133,11 +1144,6 @@ const sendMessagePortToDialogWorker = async port => {
1133
1144
  await invokeAndTransfer('SendMessagePortToExtensionHostWorker.sendMessagePortToDialogWorker', port, command);
1134
1145
  };
1135
1146
 
1136
- const Electron = 2;
1137
-
1138
- const DownloadingUpdate = 2;
1139
- const DownloadedUpdate = 3;
1140
-
1141
1147
  const emptyObject = {};
1142
1148
  const RE_PLACEHOLDER = /\{(PH\d+)\}/g;
1143
1149
  const i18nString = (key, placeholders = emptyObject) => {
@@ -1271,6 +1277,30 @@ const getDiskPath = async downloadUrl => {
1271
1277
  return diskPath;
1272
1278
  };
1273
1279
 
1280
+ const isGreater = (version, otherVersion) => {
1281
+ const versionParts = version.split('.').map(Number);
1282
+ const otherVersionParts = otherVersion.split('.').map(Number);
1283
+ const partCount = Math.max(versionParts.length, otherVersionParts.length);
1284
+ for (let index = 0; index < partCount; index++) {
1285
+ const versionPart = versionParts[index] || 0;
1286
+ const otherVersionPart = otherVersionParts[index] || 0;
1287
+ if (versionPart !== otherVersionPart) {
1288
+ return versionPart > otherVersionPart;
1289
+ }
1290
+ }
1291
+ return false;
1292
+ };
1293
+
1294
+ const getCurrentVersion = async () => {
1295
+ // @ts-ignore
1296
+ const override = await invoke('Preferences.get', 'update.emulateCurrentVersion');
1297
+ if (typeof override === 'string' && /^\d+\.\d+\.\d+$/.test(override)) {
1298
+ return override;
1299
+ }
1300
+ // @ts-ignore
1301
+ return invoke('Process.getVersion');
1302
+ };
1303
+
1274
1304
  const parseVersionFromUrl = (url, repository) => {
1275
1305
  if (!url.includes('releases/tag')) {
1276
1306
  if (url.endsWith('/releases')) {
@@ -1301,6 +1331,15 @@ const getLatestReleaseVersion = async repository => {
1301
1331
  }
1302
1332
  };
1303
1333
 
1334
+ const getLatestVersion = async repository => {
1335
+ const currentVersion = await getCurrentVersion();
1336
+ const release = await getLatestReleaseVersion(repository);
1337
+ if (isGreater(release.version, currentVersion)) {
1338
+ return release;
1339
+ }
1340
+ return undefined;
1341
+ };
1342
+
1304
1343
  const getUpdateUrl = (repository, fileNameTemplate, version) => {
1305
1344
  const fileName = fileNameTemplate.replace('${version}', () => version);
1306
1345
  const url = `https://github.com/${repository}/releases/download/v${version}/${fileName}`;
@@ -1342,6 +1381,103 @@ const installAndRestart = async downloadPath => {
1342
1381
  }
1343
1382
  };
1344
1383
 
1384
+ const downloadMacUpdate = async (version, arch) => {
1385
+ if (!/^\d+\.\d+\.\d+$/.test(version) || !['arm64', 'x64'].includes(arch)) {
1386
+ throw new Error('Invalid macOS update version or architecture');
1387
+ }
1388
+ const repository = 'lvce-editor/lvce-editor';
1389
+ const assetName = `lvce-v${version}_${arch}.dmg`;
1390
+ const url = `https://github.com/${repository}/releases/download/v${version}/${assetName}`;
1391
+ const metadata = await fetch(`https://api.github.com/repos/${repository}/releases/tags/v${version}`, {
1392
+ headers: {
1393
+ Accept: 'application/vnd.github+json'
1394
+ },
1395
+ signal: AbortSignal.timeout(30_000)
1396
+ });
1397
+ if (!metadata.ok) {
1398
+ throw new Error(`Failed to read release metadata: HTTP ${metadata.status}`);
1399
+ }
1400
+ const release = await metadata.json();
1401
+ const asset = release.assets?.find(item => item.name === assetName);
1402
+ const maxSize = 512 * 1024 * 1024;
1403
+ if (!asset || asset.browser_download_url !== url || !/^sha256:[a-f0-9]{64}$/.test(asset.digest) || !(asset.size > 0 && asset.size <= maxSize)) {
1404
+ throw new Error(`Release is missing a valid ${assetName} asset or SHA-256 digest`);
1405
+ }
1406
+ const response = await fetch(url, {
1407
+ signal: AbortSignal.timeout(600_000)
1408
+ });
1409
+ if (!response.ok || !response.body) {
1410
+ throw new Error(`Failed to download update: HTTP ${response.status}`);
1411
+ }
1412
+ const reader = response.body.getReader();
1413
+ const chunks = [];
1414
+ let size = 0;
1415
+ try {
1416
+ while (true) {
1417
+ const {
1418
+ done,
1419
+ value
1420
+ } = await reader.read();
1421
+ if (done) {
1422
+ break;
1423
+ }
1424
+ size += value.byteLength;
1425
+ if (size > asset.size) {
1426
+ throw new Error('Update download exceeds the expected size');
1427
+ }
1428
+ chunks.push(new Uint8Array(value).buffer);
1429
+ }
1430
+ } finally {
1431
+ await reader.cancel();
1432
+ }
1433
+ const blob = new Blob(chunks);
1434
+ const hash = await crypto.subtle.digest('SHA-256', await blob.arrayBuffer());
1435
+ const digest = `sha256:${Array.from(new Uint8Array(hash), byte => byte.toString(16).padStart(2, '0')).join('')}`;
1436
+ if (size !== asset.size || digest !== asset.digest) {
1437
+ throw new Error('Update download checksum or size does not match');
1438
+ }
1439
+ const diskPath = await getDiskPath(url);
1440
+ await downloadToDisk(diskPath, new Response(blob));
1441
+ return diskPath;
1442
+ };
1443
+
1444
+ const installMacUpdate = async (version, download = downloadMacUpdate) => {
1445
+ const install = await invoke$1('ConfirmPrompt.prompt', `Version ${version} is available. Install this update?`, {
1446
+ confirmMessage: 'Install Update',
1447
+ platform: Electron
1448
+ });
1449
+ if (!install) {
1450
+ return {
1451
+ error: undefined,
1452
+ updated: false
1453
+ };
1454
+ }
1455
+ // @ts-ignore
1456
+ await invoke('Notification.create', 'info', `Preparing update ${version}...`);
1457
+ // @ts-ignore
1458
+ const arch = await invoke('Process.getArch');
1459
+ const diskPath = await download(version, arch);
1460
+ // @ts-ignore
1461
+ await invoke('AutoUpdater.stageMacUpdate', diskPath, version);
1462
+ const restart = await invoke$1('ConfirmPrompt.prompt', `Update ${version} is ready. Restart to finish installing?`, {
1463
+ cancelMessage: 'Later',
1464
+ confirmMessage: 'Restart',
1465
+ platform: Electron
1466
+ });
1467
+ if (!restart) {
1468
+ return {
1469
+ error: undefined,
1470
+ updated: false
1471
+ };
1472
+ }
1473
+ // @ts-ignore
1474
+ await invoke('AutoUpdater.restartMacUpdate');
1475
+ return {
1476
+ error: undefined,
1477
+ updated: true
1478
+ };
1479
+ };
1480
+
1345
1481
  const isCached = async (url, cache) => {
1346
1482
  const match = await cache.match(url);
1347
1483
  return !!match;
@@ -1368,13 +1504,18 @@ const doCheckForUpdates = async (updateSetting, repository, fileNameTemplate, bu
1368
1504
  updated: false
1369
1505
  };
1370
1506
  }
1371
- const info = await getLatestReleaseVersion(repository);
1507
+ const info = await getLatestVersion(repository);
1372
1508
  if (!info || !info.version) {
1373
1509
  return {
1374
1510
  error: undefined,
1375
1511
  updated: false
1376
1512
  };
1377
1513
  }
1514
+ // @ts-ignore
1515
+ const platform = await invoke('AutoUpdater.getPlatform');
1516
+ if (platform === 'darwin') {
1517
+ return await installMacUpdate(info.version);
1518
+ }
1378
1519
  const cache = await getCache(bucketName, cacheName);
1379
1520
  if (!(await shouldUpdate(updateSetting, info.version))) {
1380
1521
  return {
@@ -1424,7 +1565,7 @@ const doCheckForUpdates = async (updateSetting, repository, fileNameTemplate, bu
1424
1565
  } catch (error) {
1425
1566
  console.error(error);
1426
1567
  return {
1427
- error: undefined,
1568
+ error: error instanceof Error ? error.message : String(error),
1428
1569
  updated: false
1429
1570
  };
1430
1571
  }
@@ -1457,7 +1598,8 @@ const checkForUpdates = async (updateSetting, repository) => {
1457
1598
 
1458
1599
  const commandMap = {
1459
1600
  'Update.checkForUpdates': checkForUpdates,
1460
- 'Update.downloadToCache': downloadUpdateToCache
1601
+ 'Update.downloadToCache': downloadUpdateToCache,
1602
+ 'Update.getLatestVersion': getLatestVersion
1461
1603
  };
1462
1604
 
1463
1605
  const listen = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/update-worker",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "description": "Update Worker",
5
5
  "repository": {
6
6
  "type": "git",