Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/live-update-electron-platform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@capawesome/capacitor-live-update": minor
---

feat: add support for the Electron platform via [`@capawesome/capacitor-electron`](https://github.com/capawesome-team/capacitor-electron)
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
id: changesets
uses: changesets/action@v1
with:
version: npm run version
publish: npm run release
commit: "chore(release): publish"
title: "chore(release): publish"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"fmt": "turbo run fmt",
"docgen": "turbo run docgen",
"build": "turbo run build",
"version": "turbo run version",
"version": "changeset version && turbo run version",
"ios:pod:install": "turbo run ios:pod:install --concurrency=1 --no-cache",
"ios:spm:install": "turbo run ios:spm:install --concurrency=1 --no-cache",
"affected:verify": "npm run affected:verify:android && npm run affected:verify:ios && npm run affected:verify:web",
Expand Down
156 changes: 99 additions & 57 deletions packages/live-update/README.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions packages/live-update/electron/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
input: 'electron/dist/esm/electron/src/index.js',
output: {
file: 'electron/dist/plugin.mjs',
format: 'esm',
},
external: [
'electron',
'@capacitor/core',
'@capawesome/capacitor-electron/plugin',
'@capawesome/electron-live-update/engine',
/^node:/,
],
};
289 changes: 289 additions & 0 deletions packages/live-update/electron/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import type { PluginsConfig } from '@capacitor/cli';
import { CapacitorException, ExceptionCode } from '@capacitor/core';
import { ElectronPlugin } from '@capawesome/capacitor-electron/plugin';
import type { ElectronPluginContext } from '@capawesome/capacitor-electron/plugin';
import { LiveUpdateEngine } from '@capawesome/electron-live-update/engine';
import { app, powerMonitor } from 'electron';
import { createRequire } from 'node:module';
import { join } from 'node:path';

import type {
DeleteBundleOptions,
DownloadBundleOptions,
FetchChannelsOptions,
FetchChannelsResult,
FetchLatestBundleOptions,
FetchLatestBundleResult,
GetBlockedBundlesResult,
GetBundlesResult,
GetChannelResult,
GetConfigResult,
GetCurrentBundleResult,
GetCustomIdResult,
GetDeviceIdResult,
GetDownloadedBundlesResult,
GetNextBundleResult,
GetVersionCodeResult,
GetVersionNameResult,
IsSyncingResult,
LiveUpdatePlugin,
ReadyResult,
SetChannelOptions,
SetConfigOptions,
SetCustomIdOptions,
SetNextBundleOptions,
SyncOptions,
SyncResult,
} from '../../src/definitions';

const AUTO_UPDATE_MIN_INTERVAL = 15 * 60 * 1000;
const CAPACITOR_RUNTIME = 'capacitor';
const ELECTRON_PLATFORM = '2';
// Resolve the plugin version from the package's own `package.json`. The built
// file lives at `electron/dist/plugin.mjs`, so the package root is two levels up.
const PLUGIN_VERSION: string = createRequire(import.meta.url)(
'../../package.json',
).version;
Comment thread
robingenz marked this conversation as resolved.

type LiveUpdateConfig = NonNullable<PluginsConfig['LiveUpdate']>;

export class LiveUpdateElectron
extends ElectronPlugin
implements Omit<LiveUpdatePlugin, 'addListener' | 'removeAllListeners'>
{
public static readonly __capacitorElectronPlugin = {
name: 'LiveUpdate',
methods: [
'clearBlockedBundles',
'deleteBundle',
'downloadBundle',
'fetchChannels',
'fetchLatestBundle',
'getBlockedBundles',
'getBundles',
'getChannel',
'getConfig',
'getCurrentBundle',
'getCustomId',
'getDeviceId',
'getDownloadedBundles',
'getNextBundle',
'getVersionCode',
'getVersionName',
'isSyncing',
'ready',
'reload',
'reset',
'resetConfig',
'setChannel',
'setConfig',
'setCustomId',
'setNextBundle',
'sync',
],
};

private static readonly errorNotImplemented = 'Not implemented on Electron.';

private readonly config: LiveUpdateConfig;
private readonly engine: LiveUpdateEngine;
private lastAutoUpdateCheck = 0;

constructor(context: ElectronPluginContext) {
super(context);
this.config =
(context.config.plugins as PluginsConfig | undefined)?.LiveUpdate ?? {};
this.engine = new LiveUpdateEngine({
appId: this.config.appId,
autoBlockRolledBackBundles: this.config.autoBlockRolledBackBundles,
autoDeleteBundles: this.config.autoDeleteBundles,
dataDirectory: join(app.getPath('userData'), 'capawesome-live-update'),
defaultChannel: this.config.defaultChannel,
httpTimeout: this.config.httpTimeout,
osVersion: process.getSystemVersion(),
platform: ELECTRON_PLATFORM,
pluginVersion: PLUGIN_VERSION,
publicKey: this.config.publicKey,
readyTimeout: this.config.readyTimeout,
runtime: CAPACITOR_RUNTIME,
sdkVersion: PLUGIN_VERSION,
serverDomain: this.config.serverDomain,
versionCode: app.getVersion(),
versionName: app.getVersion(),
});
this.engine.on('downloadBundleProgress', event =>
this.context.notifyListeners('downloadBundleProgress', event),
);
this.engine.on('nextBundleSet', event =>
this.context.notifyListeners('nextBundleSet', event),
);
// The engine owns rollback. When its ready watchdog rolls back,
// repoint the platform so that all windows reload the target bundle.
this.engine.on('rolledBack', () => {
void this.applyCurrentBundle().catch(error =>
console.error(`[LiveUpdate] Failed to apply rollback: ${error}`),
);
});
}

public async clearBlockedBundles(): Promise<void> {
return this.engine.clearBlockedBundles();
}

public async deleteBundle(options: DeleteBundleOptions): Promise<void> {
return this.engine.deleteBundle(options);
}

public async downloadBundle(options: DownloadBundleOptions): Promise<void> {
return this.engine.downloadBundle(options);
}

public async fetchChannels(
options?: FetchChannelsOptions,
): Promise<FetchChannelsResult> {
return this.engine.fetchChannels(options);
}

public async fetchLatestBundle(
options?: FetchLatestBundleOptions,
): Promise<FetchLatestBundleResult> {
return this.engine.fetchLatestBundle(options);
}

public async getBlockedBundles(): Promise<GetBlockedBundlesResult> {
return this.engine.getBlockedBundles();
}

public async getBundles(): Promise<GetBundlesResult> {
return this.getDownloadedBundles();
}

public async getChannel(): Promise<GetChannelResult> {
return this.engine.getChannel();
}

public async getConfig(): Promise<GetConfigResult> {
return {
appId: this.config.appId ?? null,
autoUpdateStrategy: this.config.autoUpdateStrategy ?? 'none',
};
}

public async getCurrentBundle(): Promise<GetCurrentBundleResult> {
return this.engine.getCurrentBundle();
}

public async getCustomId(): Promise<GetCustomIdResult> {
return this.engine.getCustomId();
}

public async getDeviceId(): Promise<GetDeviceIdResult> {
return this.engine.getDeviceId();
}

public async getDownloadedBundles(): Promise<GetDownloadedBundlesResult> {
return this.engine.getDownloadedBundles();
}

public async getNextBundle(): Promise<GetNextBundleResult> {
return this.engine.getNextBundle();
}

public async getVersionCode(): Promise<GetVersionCodeResult> {
return this.engine.getVersionCode();
}

public async getVersionName(): Promise<GetVersionNameResult> {
return this.engine.getVersionName();
}

public async load(): Promise<void> {
await this.engine.initialize();
const bundlePath = this.engine.getCurrentBundlePath();
if (
bundlePath !== null ||
this.context.services.bundles.getActiveBundlePath() !== null
) {
await this.context.services.bundles.setActiveBundle(bundlePath, {
bootWatchdog: false,
});
}
if (this.config.autoUpdateStrategy === 'background') {
this.setUpBackgroundAutoUpdate();
}
}

public async isSyncing(): Promise<IsSyncingResult> {
return this.engine.isSyncing();
}

public async ready(): Promise<ReadyResult> {
return this.engine.ready();
}

public async reload(): Promise<void> {
await this.engine.applyNextBundle();
await this.applyCurrentBundle();
this.context.notifyListeners('reloaded');
}

public async reset(): Promise<void> {
return this.engine.reset();
}

public async resetConfig(): Promise<void> {
this.throwUnimplementedError();
}

public async setChannel(options: SetChannelOptions): Promise<void> {
return this.engine.setChannel(options);
}

public async setConfig(_options: SetConfigOptions): Promise<void> {
this.throwUnimplementedError();
}

public async setCustomId(options: SetCustomIdOptions): Promise<void> {
return this.engine.setCustomId(options);
}

public async setNextBundle(options: SetNextBundleOptions): Promise<void> {
return this.engine.setNextBundle(options);
}

public async sync(options?: SyncOptions): Promise<SyncResult> {
return this.engine.sync(options);
}

private async applyCurrentBundle(): Promise<void> {
await this.context.services.bundles.setActiveBundle(
this.engine.getCurrentBundlePath(),
{ bootWatchdog: false },
);
}

private setUpBackgroundAutoUpdate(): void {
const check = (): void => {
const now = Date.now();
if (now - this.lastAutoUpdateCheck < AUTO_UPDATE_MIN_INTERVAL) {
return;
}
this.lastAutoUpdateCheck = now;
void this.sync().catch(error =>
console.warn(`[LiveUpdate] Background sync failed: ${error}`),
);
};
void app.whenReady().then(() => {
check();
app.on('browser-window-focus', check);
powerMonitor.on('resume', check);
});
}

private throwUnimplementedError(): never {
throw new CapacitorException(
LiveUpdateElectron.errorNotImplemented,
ExceptionCode.Unimplemented,
);
}
}
38 changes: 38 additions & 0 deletions packages/live-update/electron/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"compilerOptions": {
"allowUnreachableCode": false,
"baseUrl": ".",
"esModuleInterop": true,
"lib": ["es2022"],
"module": "esnext",
"moduleResolution": "bundler",
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist/esm",
"paths": {
// TypeScript tries each candidate in order and uses the first one
// that exists on disk. Once `@capawesome/electron-live-update` is
// published, the installed package (package-level or hoisted to the
// workspace root `node_modules`) is used; the sibling repository
// checkout is only a fallback for local development before then.
"@capawesome/electron-live-update/engine": [
"../node_modules/@capawesome/electron-live-update/dist/engine/index.d.ts",
"../../../node_modules/@capawesome/electron-live-update/dist/engine/index.d.ts",
"../../../../electron-live-update/dist/engine/index.d.ts"
],
"@capawesome/capacitor-electron/plugin": [
"../node_modules/@capawesome/capacitor-electron/dist/types/plugin/index.d.ts",
"../../../node_modules/@capawesome/capacitor-electron/dist/types/plugin/index.d.ts",
"../../../../capacitor-electron/dist/types/plugin/index.d.ts"
]
},
"pretty": true,
"rootDir": "..",
"skipLibCheck": true,
"strict": true,
"target": "es2022",
"types": []
},
"include": ["src/**/*.ts"]
}
6 changes: 6 additions & 0 deletions packages/live-update/example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ dist/

*.pem
*.crt

# Electron e2e (temporary local scaffolding)
.local-tarballs/
test/electron/.fixtures/
test/electron/.userdata/
test/electron/test-results/
6 changes: 6 additions & 0 deletions packages/live-update/example/electron/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
build
dist
app
generated
vendor
9 changes: 9 additions & 0 deletions packages/live-update/example/electron/assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Build resources

Place your app icons here for packaging with electron-builder:

- `icon.icns` — macOS (or a 1024x1024 `icon.png`, converted automatically)
- `icon.ico` — Windows
- `icon.png` — Linux (512x512 or larger)

See https://www.electron.build/configuration/icons for details.
Loading
Loading